Out-of-range numeric conversions silently wrap instead of erroring (ToInt64E(uint64max) == -1)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 4k
- Forks
- 337
- Avg merge
- 15m
- Merged PRs (30d)
- 1
Description
Every To<Int>E / To<Uint>E conversion from a typed numeric value is an unchecked Go conversion. Out-of-range inputs wrap silently and return err == nil. The same values arriving as strings are correctly rejected, so the two paths through the same function disagree.
Observed on cast v1.10.0, go1.24, linux/amd64.
Reproduction
package main
import (
"fmt"
"math"
"github.com/spf13/cast"
)
func main() {
fmt.Println(cast.ToInt64E(uint64(math.MaxUint64))) // -1 <nil>
fmt.Println(cast.ToInt64E(float64(math.MaxInt64))) // -9223372036854775808 <nil>
fmt.Println(cast.ToInt64E(1e300)) // -9223372036854775808 <nil>
fmt.Println(cast.ToInt64E(math.Inf(1))) // -9223372036854775808 <nil>
fmt.Println(cast.ToInt64E(math.NaN())) // -9223372036854775808 <nil>
fmt.Println(cast.ToInt8E(1e300)) // 0 <nil>
fmt.Println(cast.ToUint64E(math.NaN())) // 9223372036854775808 <nil>
// the string path, same values, correctly rejected:
fmt.Println(cast.ToInt64E("1e300")) // 0 unable to cast ...
}
| input | ToInt64E |
error? |
|---|---|---|
uint64(math.MaxUint64) |
-1 |
no |
float64(math.MaxInt64) |
-9223372036854775808 |
no |
1e300 |
-9223372036854775808 |
no |
math.Inf(1) |
-9223372036854775808 |
no |
math.NaN() |
-9223372036854775808 |
no |
"1e300" (string) |
0 |
yes |
The first one has no float in it at all — it is a plain integer wraparound.
Cause
toNumber[T] converts every typed numeric case with a bare T(s) and unconditionally reports success:
func toNumber[T Number](i any) (T, bool) {
i, _ = indirect(i)
switch s := i.(type) {
case T:
return s, true
...
case uint64:
return T(s), true // wraps
case float64:
return T(s), true // out-of-range float -> int is implementation-defined
...
}
return 0, false
}
toNumberE only reaches parseFn (strconv.ParseInt with an explicit bit size, which does range-check) when toNumber returns ok == false — i.e. for strings and json.Number. So the checked path is only ever taken for text input.
Per the Go spec, converting a float to an integer type when the value is out of range is implementation-defined; on amd64 and arm64 it yields the "integer indefinite" value, math.MinInt64.
That the unsigned helper already returns errNegativeNotAllowed for negative input suggests out-of-range is meant to be an error condition here — the range check simply never got applied to the typed path.
Why it is reachable in practice
spf13/viper is built on cast, and encoding/json decodes every JSON number to a float64. So an ordinary config file silently produces a negative limit:
v := viper.New()
v.SetConfigType("json")
v.ReadConfig(bytes.NewBufferString(`{"max_bytes": 9223372036854775807}`))
v.GetInt64("max_bytes") // -9223372036854775808
The identical value in YAML returns 9223372036854775807, because yaml.v3 decodes to a real int64 and never enters the float path. So the same configuration behaves differently depending on the file format — which is the part most likely to cost someone a long afternoon.
Verified with viper v1.21.0 and cast v1.10.0.
Possible fix
Range-check in toNumber before converting, and let the caller turn ok == false into the existing error. For the float cases the bound has to be written against 2^63 rather than math.MaxInt64 — the untyped constant converts to the same float64 as the values that overflow, so f > math.MaxInt64 is false exactly when it matters:
const maxInt64AsFloat = float64(1 << 63) // exactly representable; math.MaxInt64 is not
if math.IsNaN(f) || f >= maxInt64AsFloat || f < float64(math.MinInt64) {
return 0, false
}
Happy to open a PR if the approach sounds right — though given it touches every To*E and changes previously-silent behaviour into errors, you may prefer to decide the shape (and whether it is a breaking change) first.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with toNumber and toNumberE, especially the typed numeric cases and the parseFn fallback described in the issue. Confirm the intended range-checking and compatibility decision for all To*E conversions; done means out-of-range typed values return errors like rejected string inputs without changing valid conversions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100