From 258fd816bab1f2db7e0d85690ee67cb047dc2fa8 Mon Sep 17 00:00:00 2001 From: suoyi Date: Tue, 13 Oct 2026 23:05:05 +0800 Subject: [PATCH] fix: reject values at or above 2^63 in int parameter parsing toInt's upper bound compares against math.MaxInt, which is 2^(strconv.IntSize-1) - 1 and cannot be represented as a float64: on 64-bit targets it rounds up to exactly 2^63, so a value that parses to 2^63 is not greater than the bound and passed the check. It then reached int(f), and converting an out-of-range float64 to int is implementation-defined - MinInt on amd64 - so the parameter was accepted and a large negative number was sent to the API instead of failing the range check. That covered every caller of RequiredInt, OptionalIntParam, OptionalIntParamWithDefault and OptionalPaginationParams, including the string "9223372036854775807", which ParseFloat rounds to 2^63. Compare against math.Ldexp(1, strconv.IntSize-1) instead. It is the exact power of two on both 32- and 64-bit targets, so the bound holds everywhere and the conversion is never reached with an out-of-range value - unlike inspecting the result of the conversion, which only catches the overflow on targets that do not saturate. --- pkg/github/params.go | 9 ++++++++- pkg/github/params_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/github/params.go b/pkg/github/params.go index 416020abb7..6e62885293 100644 --- a/pkg/github/params.go +++ b/pkg/github/params.go @@ -83,7 +83,14 @@ func toInt(val any) (int, error) { if f != math.Trunc(f) { return 0, fmt.Errorf("non-integer numeric value: %v", f) } - if f > math.MaxInt || f < math.MinInt { + // Compare before converting, against the exact power of two. math.MaxInt is + // 2^(strconv.IntSize-1) - 1, which a float64 cannot represent: on 64-bit + // targets it rounds up to exactly 2^63, so comparing against it lets 2^63 + // through. Converting an out-of-range float64 to int is implementation-defined + // - MinInt on amd64, MaxInt on the targets whose conversion saturates - so the + // bound has to reject the value before the conversion rather than inspect the + // result of it. math.Ldexp gives the exact bound on every target. + if limit := math.Ldexp(1, strconv.IntSize-1); f >= limit || f < -limit { return 0, fmt.Errorf("numeric value out of int range: %v", f) } return int(f), nil diff --git a/pkg/github/params_test.go b/pkg/github/params_test.go index 3c0d7f43d1..fe70a9d1ae 100644 --- a/pkg/github/params_test.go +++ b/pkg/github/params_test.go @@ -376,6 +376,34 @@ func Test_OptionalIntParam(t *testing.T) { expected: 0, expectError: true, }, + { + name: "2^63 string parameter", + params: map[string]any{"count": "9223372036854775808"}, + paramName: "count", + expected: 0, + expectError: true, + }, + { + name: "MaxInt64 string parameter rounds up to 2^63", + params: map[string]any{"count": "9223372036854775807"}, + paramName: "count", + expected: 0, + expectError: true, + }, + { + name: "2^63 float64 parameter", + params: map[string]any{"count": math.Pow(2, 63)}, + paramName: "count", + expected: 0, + expectError: true, + }, + { + name: "MaxInt32 float64 parameter", + params: map[string]any{"count": float64(math.MaxInt32)}, + paramName: "count", + expected: math.MaxInt32, + expectError: false, + }, } for _, tc := range tests {