fix: signed-shift overflow in hdr_calculate_bucket_config (UBSan, found by fuzzing) - #145
Conversation
ClusterFuzzLite (UBSan) found a signed-left-shift overflow in hdr_calculate_bucket_config: sub_bucket_mask was computed as ((int64_t)sub_bucket_count - 1) << unit_magnitude *before* the (unit_magnitude + sub_bucket_half_count_magnitude > 61) guard that rejects out-of-range configs. A crafted decoded log with lowest_discernible_value ~2^56 (reachable via hdr_log_read -> hdr_decode_compressed_v0 -> hdr_init) shifts 255 by 56 places, which is undefined behaviour for int64_t. Move the guard before the shift so such configs return EINVAL without ever performing the overflowing shift. No change for valid inputs. Found-by: ClusterFuzzLite batch fuzzing (log_reader_fuzzer, UBSan) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review of the sub_bucket_mask shift fix surfaced a sibling signed-overflow in the same validation: 'lowest_discernible_value * 2 > highest_trackable_value' overflows int64_t when a crafted/decoded lowest_discernible_value is near INT64_MAX (reachable via hdr_decode_compressed_v0/v1 -> hdr_init). Rewrite as 'lowest_discernible_value > highest_trackable_value / 2', which is equivalent for all non-overflowing inputs (lowest >= 1 is already checked) and cannot overflow. Verified under UBSan: (INT64_MAX,100) and (2^62+1,100) now return EINVAL with no UB; valid/edge cases unchanged. Found-by: adversarial review of the fuzzing fix (ClusterFuzzLite, UBSan) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t job - test/hdr_histogram_test.c: assert hdr_init rejects (EINVAL, no crash) a config that would overflow the sub_bucket_mask shift (lowest=2^56); this aborts under UBSan on the pre-fix code. - test/regression-bucket-config-shift-overflow.hlog: the fuzzer's crash reproducer, auto-added to the log_reader_fuzzer seed corpus via build.sh. - .github/workflows/ci.yml: new 'sanitizers' job runs the unit suite under ASan+UBSan so this class of bug (integer overflow / OOB) is caught deterministically per-PR, not only in the weekly fuzzing run. Verified the full suite is clean under both sanitizers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b63351d to
e912c8d
Compare
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. The core fix looks right. Moving the The three Two smaller things. The I read the sources to check the arithmetic equivalence and the recorder ownership — |
…code; enable LeakSanitizer in CI Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alized read via two-step init) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without it a missing zlib silently builds the no-op log backend and skips the decode leak tests this PR fixes; ON makes that fail loudly.
Found by fuzzing
The new weekly ClusterFuzzLite batch run (UBSan) flagged undefined behaviour in
hdr_calculate_bucket_config:Root cause
sub_bucket_maskwas computed as((int64_t) sub_bucket_count - 1) << unit_magnitudebefore the guardif (unit_magnitude + sub_bucket_half_count_magnitude > 61) return EINVAL;. A crafted, decoded log entry can producelowest_discernible_value ≈ 2^56→unit_magnitude == 56,sub_bucket_count == 256, so the code shifts255 << 56, which overflowsint64_t— signed-left-shift UB — before the config is rejected.Fix
Move the
> 61guard above thesub_bucket_maskshift, so an out-of-range config returnsEINVALwithout ever performing the overflowing shift. The threshold is unchanged and correct:(sub_bucket_count - 1)occupies(sub_bucket_half_count_magnitude + 1)bits, so the shift's top bit isunit_magnitude + sub_bucket_half_count_magnitude; keeping that≤ 61leaves a representable, non-negativeint64_t. No behavior change for valid inputs.Verification
EINVALpath).🤖 Generated with Claude Code