From 4384ea1afbf983615961ce99533b3b703bbaeac4 Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Fri, 20 Mar 2026 16:53:13 +0100 Subject: [PATCH 1/2] fix(fuzz): skip sample validation for step-invariant expressions The native histogram fuzzer found a samples-per-step mismatch when queries use the @ modifier (e.g. predict_linear({...}[2m] @ 0.000)). The Thanos engine's step invariant operator caches the first evaluation and replays it for subsequent steps without re-counting samples, while Prometheus counts samples at every step. This is an optimization difference, not a correctness issue. Detect @ modifier usage via VectorSelector.Timestamp/StartOrEnd fields and skip sample comparison for those expressions. Constraint: step invariant operator uses sync.Once for caching, samples only counted on first batch Rejected: Fix sample counting in step invariant operator | would add overhead for non-functional stat tracking Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Sylvain Rabot --- engine/enginefuzz_test.go | 9 +++++++++ .../fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 diff --git a/engine/enginefuzz_test.go b/engine/enginefuzz_test.go index a1c60d563..0f9574778 100644 --- a/engine/enginefuzz_test.go +++ b/engine/enginefuzz_test.go @@ -56,6 +56,15 @@ func shouldValidateSamples(expr parser.Expr) bool { parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { switch n := node.(type) { + case *parser.VectorSelector: + if n.Timestamp != nil || n.StartOrEnd != 0 { + // The Thanos engine's step invariant operator caches the result of the + // first evaluation and replays it for subsequent steps without re-counting + // samples. This leads to fewer total samples compared to Prometheus which + // counts samples at every step. + valid = false + return errors.New("error") + } case *parser.Call: switch n.Func.Name { case "scalar": diff --git a/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 b/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 new file mode 100644 index 000000000..e3baa6eb3 --- /dev/null +++ b/engine/testdata/fuzz/FuzzNativeHistogramQuery/df60c8694f1c9759 @@ -0,0 +1,10 @@ +go test fuzz v1 +int64(-57) +uint32(27) +uint32(248) +uint32(12) +int8(0) +int8(0) +uint64(1) +uint64(1) +uint64(16) From 0654c4c17d7895695681ac3b90273c3b7d144997 Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Fri, 20 Mar 2026 16:55:08 +0100 Subject: [PATCH 2/2] fix(fuzz): exclude count_values from fuzzer validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit count_values converts float values to string labels, amplifying tiny floating point precision differences between engines into label mismatches. For example, stdvar_over_time may compute 61.24999999999997 vs 61.24999999999998 — both correct within float64 precision, but producing different count_values labels. Constraint: float64 has ~15-16 significant digits; different evaluation order yields different last-digit results Rejected: Fix float precision in stdvar_over_time | both values are equally valid within IEEE 754 Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Sylvain Rabot --- engine/enginefuzz_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/engine/enginefuzz_test.go b/engine/enginefuzz_test.go index 0f9574778..3557612da 100644 --- a/engine/enginefuzz_test.go +++ b/engine/enginefuzz_test.go @@ -90,6 +90,14 @@ func validateExpr(expr parser.Expr, testType testType) bool { parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { switch n := node.(type) { + case *parser.AggregateExpr: + if n.Op == parser.COUNT_VALUES { + // count_values converts float values to string labels. Tiny floating point + // precision differences between engines (e.g. 61.24999999999997 vs 61.24999999999998) + // produce different label values, causing result mismatches. + valid = false + return errors.New("error") + } case *parser.Call: switch n.Func.Name { case "sort", "sort_desc", "sort_by_label", "sort_by_label_desc":