Skip to content

fix: harden query path — hdr_mean overflow + count accessor OOB reads - #147

Open
filipecosta90 wants to merge 3 commits into
mainfrom
fix/mean-and-count-bounds
Open

filipecosta90 wants to merge 3 commits into
mainfrom
fix/mean-and-count-bounds

Conversation

@filipecosta90

Copy link
Copy Markdown
Contributor

Two more fuzzing-surfaced bugs in the query path (found by adversarial review during the ClusterFuzzLite effort).

1. hdr_mean — signed int64 overflow (UBSan), fuzz-reachable

total += iter.count * hdr_median_equivalent_value(...) accumulated in int64. For a histogram holding values near 2^62, a few samples overflow INT64_MAX → signed-overflow UB and a wrapped/negative mean. log_reader_fuzzer calls hdr_mean on every decoded histogram, so a crafted large-range log reaches it. Fix: accumulate in double (exactly what hdr_stddev already does).

2. hdr_count_at_value — out-of-bounds read (ASan)

It indexed counts[] via counts_index_for() with no range check, so querying a value beyond highest_trackable_value read past the array. Fix: mirror hdr_record_values' guard (value range + (uint32_t)index >= (uint32_t)counts_len) and return 0 — an untracked value has count 0.

Verification (local, ASan+UBSan)

  • Before: hdr_meanruntime error: signed integer overflow ... at hdr_histogram.c:832; hdr_count_at_value(h, INT64_MAX) → ASan heap-buffer-overflow READ.
  • After: hdr_mean returns a positive finite value; hdr_count_at_value returns 0. Added regression tests; full suite passes under ASan+UBSan.

The ASan/UBSan ctest CI job that makes these regression tests deterministic guards is added in #145.

🤖 Generated with Claude Code

Two more issues found by adversarial review during the ClusterFuzzLite effort:

- hdr_mean summed iter.count * hdr_median_equivalent_value in an int64 running
  total. For a histogram holding values near 2^62 this overflows INT64_MAX
  (signed-overflow UB) and yields a garbage/negative mean. Reachable via the
  fuzz surface (log_reader_fuzzer calls hdr_mean on every decoded histogram).
  Accumulate in double, as hdr_stddev already does.

- hdr_count_at_value indexed counts[] via counts_index_for() with no range
  check, so a value beyond highest_trackable_value read out of bounds. Mirror
  hdr_record_values' guard and return 0 for out-of-range values.

Verified under ASan+UBSan: main aborts (UBSan signed overflow in hdr_mean;
ASan OOB read in hdr_count_at_value); both return clean results after the fix.
Added regression tests.

Found-by: adversarial review during the ClusterFuzzLite fuzzing effort
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@filipecosta90 filipecosta90 changed the title fix: hdr_mean int64 overflow (UBSan) + hdr_count_at_value OOB read (ASan) fix: harden query path — hdr_mean overflow + count accessor OOB reads Jul 23, 2026
@filipecosta90
filipecosta90 requested review from giltene and mikeb01 July 24, 2026 09:09
Adversarial review of the hdr_count_at_value fix flagged its index-based twin
hdr_count_at_index as having the identical unchecked out-of-bounds read (raw
caller index -> counts_get_normalised -> h->counts[index]). Guard it the same
way (return 0 for an out-of-range index; the unsigned compare also catches
negatives). All in-tree callers pass in-range indices.

Also strengthen the regression tests per review: pin hdr_mean to a plausible
magnitude band (not just sign/finiteness), assert in-range and the
value==highest boundary for hdr_count_at_value (catchable without a sanitizer),
and add an out-of-range test for hdr_count_at_index.

(Note: hdr_value_at_index's shift UB at an extreme index is left as a
documented precondition -- it performs no counts[] access and guarding it would
change the internal peek at index==counts_len; tracked separately.)

Found-by: adversarial review during the ClusterFuzzLite fuzzing effort
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fcostaoliveira
fcostaoliveira force-pushed the fix/mean-and-count-bounds branch from 6e627e5 to b6343fe Compare August 28, 2026 10:23
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

🤖 Automated first-pass review — a human maintainer's review is still required before merge.

Both fixes read as correct to me, and the diff is well-scoped: no change to struct hdr_histogram or to any existing public signature, so nothing here implicates the SOVERSION process in CMakeLists.txt.

hdr_mean: accumulating in double is what hdr_stddev already does in-tree, and as far as I can tell it's also what AbstractHistogram.getMean() does upstream (totalValue += medianEquivalentValue(...) * (double) countAtValueIteratedTo) — so this moves the port toward the Java reference rather than away from it. Worth confirming against the Java source before merge, since that's what usually settles these questions here. The one genuine behaviour change is that the sum is no longer exact past 2^53; for anyone diffing means against a previous release that's a last-few-bits difference, not a regression.

While in that function: hdr_mean on an empty histogram still returns NaN (0.0 / 0), where Java returns 0.0 early. Out of scope for this PR, and adjacent to #116, but the query-path hardening is the natural place for it if you want it.

hdr_count_at_value: the final form — an index-range check rather than a value > highest_trackable_value check — is the right one, and the second histogram in the test pins exactly the case a naive bound check gets wrong (a value above highest_trackable_value that is still equivalent to a tracked bucket). The explicit value < 0 guard is doing real work too: the right shift in counts_index_for on a negative value is implementation-defined and could otherwise land back inside the array.

hdr_count_at_index now returns 0 for an out-of-range index rather than reading past counts[]. That's a silent behaviour change on a public accessor rather than an assert, but it matches how hdr_count_at_value behaves for an untracked value, so it seems consistent.

One calibration on the CI claim in the description: the per-PR ClusterFuzzLite job is ASan-only and 200s, and it only reaches these accessors if a target actually calls them; the hdr_mean overflow is a UBSan finding and only the weekly batch job runs UBSan. The mean test asserts a magnitude band, so it fails on a revert without any sanitizer — good. The two count tests would only reliably catch a reverted guard under ASan, so as the description says, they depend on #145 landing to be deterministic guards; that ordering seems worth keeping in mind at merge time.

I can't verify the Java behaviour or run the sanitizers from here, so the author's stated local ASan+UBSan verification is what the mean change ultimately rests on. The statistical side may be worth a second look from whoever is comfortable comparing against the upstream Java implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants