fix: int64 overflow in linear/log iterator reporting level (near INT64_MAX) - #149
Conversation
cedc46a to
92a219f
Compare
49b2366 to
655ad34
Compare
Found by adversarial review during the ClusterFuzzLite effort. For a histogram whose highest_trackable_value is near INT64_MAX, the linear and logarithmic iterators advanced their reporting level with 'next += value_units_per_bucket' (iter_linear_next) and 'next *= (int64_t)log_base' (log_iter_next), both of which overflow int64 -> signed-overflow UB, then a negative left-shift when the wrapped value flows into lowest_equivalent_value. Saturate the reporting level at INT64_MAX when the next step would overflow. Crucially, also pin next_value_reporting_level_lowest_equivalent to INT64_MAX in that case: no bucket value ever equals INT64_MAX, so the 'iter->value >= level' emit test can no longer fire and the iterator drains its remaining buckets and terminates rather than re-reporting the saturated level forever. Verified under ASan+UBSan: linear (value_units_per_bucket=2^62) and log (base 2) over an INT64_MAX-range histogram no longer trap and terminate (2 and 64 steps); normal-range iteration is unchanged. Regression test added. Stacked on #148 (whose move_next fix the iterators depend on). Found-by: adversarial review during the ClusterFuzzLite fuzzing effort Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the reporting-level saturation found two more iterator issues in the same functions: - next_value_greater_than_reporting_level_upper_bound peeked hdr_value_at_index(h, counts_index + 1); at the last bucket that reads one past counts[], and value_from_index shifts into the sign bit -> signed-shift UB for a near-INT64_MAX range. It was masked before this PR (the reporting level overflowed first); saturating that unmasked it. Require counts_index+1 to be in range before peeking. - Degenerate iterator parameters looped forever: value_units_per_bucket <= 0 (level never advances) and log_base <= 1 (cast to 0/1, *= never advances). Pin the level (and its lowest-equivalent) to INT64_MAX in those cases too so the emit test cannot re-fire and the iterator drains and terminates. The degenerate checks are ordered first so the guard arithmetic (INT64_MAX - vpb, INT64_MAX / base) can't itself overflow or divide by zero. Verified under ASan+UBSan: the peek repro (hdr_init(1, 1<<62, 1), record 1<<62, linear vpb=INT64_MAX-1 / log base 2) and the degenerate cases (vpb=0, log_base 1.0/0.5/1.5) all terminate with no trap; normal iteration unchanged. Regression tests extended. Found-by: adversarial review during the ClusterFuzzLite fuzzing effort Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
655ad34 to
e478997
Compare
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. The core overflow fix reads correctly to me, and the diff stays on the safe side of the ABI line: no change to
On coverage: the per-PR ClusterFuzzLite job runs ASan only — UBSan is weekly-batch only — so this whole class of signed-overflow/shift UB isn't caught by the per-PR gate. The added unit test is the real regression gate here, and the 1023-step tail assertion is a good thing to have after the peek-guard revision, given that revision was itself fixing an over-eager guard. Finally, the description still says to base this on #148 and retarget to |
| while (hdr_iter_next(&iter)) | ||
| { | ||
| mu_assert("linear iterator must terminate", ++steps < 1000000); | ||
| } |
There was a problem hiding this comment.
Looks like iter.value_iterated_to remains at the previous reporting level after saturation (instead of being updated to INT64_MAX)
Should this test also assert that the final reported value is INT64_MAX?
If so, it should also apply for the log iterator test too
Stacked on #148 (the iterators call
move_next, whose value-range overflow #148 fixes). Base this PR on #148; retarget tomainonce #148 merges.Bug (found by adversarial review during the ClusterFuzzLite effort)
For a histogram with
highest_trackable_valuenearINT64_MAX, the linear and log iterators overflow int64 when advancing the reporting level:iter_linear_next(:1162):next_value_reporting_level += value_units_per_bucketlog_iter_next(:1219):next_value_reporting_level *= (int64_t) log_baseBoth are in-contract (public iterators) and reachable via any full linear/log iteration of such a histogram; the wrapped negative level then triggers a negative left-shift in
lowest_equivalent_value.Fix
Saturate the reporting level at
INT64_MAXwhen the next step would overflow. Termination: naive saturation would re-report the top level forever (the emit branch doesn't advancecounts_index). Since no bucket value ever equalsINT64_MAX, pinningnext_value_reporting_level_lowest_equivalenttoINT64_MAXas well makes theiter->value >= leveltest stop firing, so the iterator drains remaining buckets and terminates.Verification (ASan+UBSan)
units=2^62) and log (base 2) over anINT64_MAXhistogram: no trap, terminate (2 and 64 steps). Normal-range iteration unchanged. Regression test added; full suite green.🤖 Generated with Claude Code