From 22fbde51279243e0492b420cdd10db379bc7fd21 Mon Sep 17 00:00:00 2001 From: alexmchughdev Date: Thu, 7 May 2026 17:27:24 +0100 Subject: [PATCH 1/2] promql: reject NaN/Inf and fix overflow bound in duration expressions calculateDuration converts a float64 seconds value into a time.Duration nanoseconds value via "time.Duration(duration*1000) * time.Millisecond". Two related issues let invalid input slip through: 1. The bounds check used "duration > 1<<63-1 || duration < -1<<63", comparing a value-in-seconds against the int64 nanosecond range. The safe input range is +/- math.MaxInt64 / 1e9 seconds (about 292 years), roughly 1e9 times tighter. Arithmetic on legal duration literals (e.g. constructed via MUL/POW operators in DurationExpr) could produce a finite value that passed the check but overflowed during the final time.Duration conversion, yielding an implementation-defined int64 silently flowing into selector Range, OriginalOffset, and Step. 2. NaN compares false against everything, so a NaN duration produced by math.Pow(-1, 0.5) (and similar) bypassed both the negative-value check and the magnitude check, again producing an implementation-defined int64 in the conversion. Reject NaN and +/-Inf up front, and align the magnitude bound with the parser's offset_duration_expr rule (1<<63 / 1e9), which already had the correct unit. Add regression tests covering each new failure mode and a happy-path case just below the new bound. ```release-notes [BUGFIX] PromQL: Reject NaN, infinite, and out-of-range duration expressions instead of silently producing an out-of-range time.Duration. ``` Signed-off-by: alexmchughdev --- promql/durations.go | 13 ++++++++++++- promql/durations_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/promql/durations.go b/promql/durations.go index c660dbf464..3dc44fbbc6 100644 --- a/promql/durations.go +++ b/promql/durations.go @@ -87,10 +87,21 @@ func (v *durationVisitor) calculateDuration(expr parser.Expr, allowedNegative bo if err != nil { return 0, err } + // Reject NaN and infinities up front. NaN compares false against everything, + // so without this guard a NaN duration would slip past the bounds check + // below and produce an implementation-defined int64 in the time.Duration + // conversion at the end of this function. + if math.IsNaN(duration) || math.IsInf(duration, 0) { + return 0, fmt.Errorf("%d:%d: duration is NaN or infinite", expr.PositionRange().Start, expr.PositionRange().End) + } if duration <= 0 && !allowedNegative { return 0, fmt.Errorf("%d:%d: duration must be greater than 0", expr.PositionRange().Start, expr.PositionRange().End) } - if duration > 1<<63-1 || duration < -1<<63 { + // duration is in seconds; the conversion below produces nanoseconds in an + // int64, so the safe input range is +/- math.MaxInt64 / 1e9 seconds. Match + // the bound used by the parser for duration literals (see + // offset_duration_expr in generated_parser.y). + if duration > 1<<63/1e9 || duration < -(1<<63)/1e9 { return 0, fmt.Errorf("%d:%d: duration is out of range", expr.PositionRange().Start, expr.PositionRange().End) } return time.Duration(duration*1000) * time.Millisecond, nil diff --git a/promql/durations_test.go b/promql/durations_test.go index 2ade9077c8..3b31e92dd6 100644 --- a/promql/durations_test.go +++ b/promql/durations_test.go @@ -266,6 +266,37 @@ func TestCalculateDuration(t *testing.T) { }, errorMessage: "modulo by zero", }, + { + name: "NaN from negative base raised to fractional power", + expr: &parser.DurationExpr{ + LHS: &parser.NumberLiteral{Val: -1}, + RHS: &parser.NumberLiteral{Val: 0.5}, + Op: parser.POW, + }, + errorMessage: "duration is NaN or infinite", + allowedNegative: true, + }, + { + name: "infinity from huge power", + expr: &parser.DurationExpr{ + LHS: &parser.NumberLiteral{Val: 2}, + RHS: &parser.NumberLiteral{Val: 1e10}, + Op: parser.POW, + }, + errorMessage: "duration is NaN or infinite", + }, + { + name: "duration exceeds time.Duration range", + expr: &parser.NumberLiteral{Val: 1e10}, + errorMessage: "duration is out of range", + }, + { + name: "duration just below the upper bound is accepted", + expr: &parser.NumberLiteral{Val: 1e9}, + // 1e9 seconds is below 1<<63/1e9 seconds (~9.22e9), so it is + // representable as a time.Duration. The exact value is preserved. + expected: time.Duration(1e9) * time.Second, + }, } for _, tt := range tests { From d494701bf95ee42bf3b0b95bac4e4e7515ee2e2a Mon Sep 17 00:00:00 2001 From: alexmchughdev Date: Thu, 7 May 2026 18:04:35 +0100 Subject: [PATCH 2/2] Retrigger CI Signed-off-by: alexmchughdev