Repository navigation
fix: out-of-range double->int conversion in hdr_timespec_from_double (UBSan) - #154
Conversation
…(UBSan)
hdr_log_read_header feeds the "#[StartTime: %lf" field straight from the file into
hdr_timespec_from_double, which converted it with `int seconds = (int) value;`.
Converting a double that is out of int range, or non-finite, to an integer is
undefined behaviour, so any log whose StartTime is outside int (for example a
millisecond epoch written where seconds were expected) is UB during header parse:
src/hdr_time.c:92:19: runtime error: 1.40348e+12 is outside the range of
representable values of type 'int'
The int accumulator cascades two more UB sites for such a value: the
`(int) round(...)` of the resulting huge fraction, and `milliseconds * 1000000`
(gcc's UBSan reports that one first, clang reports the cast).
Range-check before converting, and widen the result to what tv_sec can actually
hold. `int` was too narrow regardless: it truncates every timestamp past 2038 even
on platforms whose tv_sec is 64-bit. The bound is derived from sizeof(tv_sec), so a
value that does not fit (32-bit tv_sec, or a non-finite/huge double) yields a
defined 0/0 rather than a trap; the fields are always written, so a caller passing
an uninitialized hdr_timespec cannot read indeterminate values on the reject path.
Millisecond resolution is unchanged, as documented in hdr_time.h.
Tests:
- test_timespec_from_double covers a normal value, a negative value, a value beyond
int that a 64-bit tv_sec holds (asserted per sizeof(tv_sec)), +/-1e300,
+/-INFINITY and NAN. Aborts under UBSan on the pre-fix code.
- handle_wide_start_time reads regression-log-start-time-overflow.hlog through
hdr_log_read_header, covering the actual reachable path; the fixture also joins
the log_reader_fuzzer seed corpus via .clusterfuzzlite/build.sh (test/*.hlog).
Verified: ctest 5/5 under gcc ASan+UBSan and under clang
ASan+UBSan+float-cast-overflow (the check the fuzzing build uses), both with
-fno-sanitize-recover=all; 5/5 gcc and clang RelWithDebInfo; 4/4 with
HDR_LOG_REQUIRED=DISABLED; no new compiler warnings; and the original
ClusterFuzzLite crash artifact now replays clean through log_reader_fuzzer.
Found-by: ClusterFuzzLite batch fuzzing (log_reader_fuzzer, UBSan)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Automated first-pass review — a human maintainer's review is still required before merge. Looks right to me. The bound comes from One small thing, no need to act on it: a StartTime that |
The Windows x86 legs emit, on the line this PR added:
hdr_time.c(109,27): warning C4244: '=': conversion from 'int64_t' to 'long',
possible loss of data
tv_sec is a long in the Windows/Cygwin hdr_timespec, so assigning an int64_t to
it narrows. The -Wall -Wextra -Wconversion comparison in the PR description could
not have caught this: it ran on LP64, where int64_t and long are the same type.
Key both the bound and the cast to long, so they agree by construction on every
platform and neither narrows. This also matches hdr_gettime's Windows branch just
above, which already does (long) integral. Deriving the bound from sizeof(long)
rather than sizeof(tv_sec) is never narrower than the int it replaces, so no
platform loses range relative to the pre-fix code; the only configuration where
the previous revision of this patch was more permissive is a 32-bit target with a
64-bit time_t, which is also exactly where it warned.
stdint.h is no longer needed here.
The test's guard moves to sizeof(long) to match, otherwise it would assert the
wide value parses on a 32-bit platform whose tv_sec is 64-bit but whose long is
not.
Verified: ctest 5/5 under gcc ASan+UBSan and clang ASan+UBSan+float-cast-overflow;
warning count on hdr_time.c back to main's 2 pre-existing; boundary sweep still
accepts up to 2^63-1024 and exactly -2^63, and rejects one past either end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Confirmed and fixed — thanks, this was a real one. I checked the Windows build logs rather than reasoning about it, and C4244 is exactly what Both x86 legs, both the static and shared targets. And the diagnosis of why my warning Fixed in 113e566 by keying both the bound and the cast to Re-verified: ctest 5/5 under gcc ASan+UBSan and under clang On the negative non-integral case — you're right that On the Java |
# Conflicts: # test/CMakeLists.txt # test/hdr_histogram_log_test.c
|
Review round: 2026-09-29. Reviewed commit: Verdict: MERGE-READY for this finite/range-checked time conversion. The destination-width correction is pushed. The range check now follows Fresh arm64 release CTest 5/5, ASan+UBSan+float-cast-overflow 5/5, and logging-disabled 4/4 pass. Linux/Windows/macOS CI, the sanitizer job, and PR fuzzing are green on this exact head. Atomic recording, offsets and the histogram layout are unchanged; no recording/scan performance claim is made. Merge #154 before #156, or use #156 as the combined fix: #156 includes normalization and the same width correction. Negative fractions remain unnormalized in this intentionally narrower PR. This is a scoped PR verdict, not certification of every input or of the entire release. |
| t->tv_sec = 0; | ||
| t->tv_nsec = 0; |
There was a problem hiding this comment.
We're "silently" mapping invalid or out of range input values to zero (tbh, I'm not totally aware of the impact)
Maybe we could return an error instead and propagate it above (?)
The down-side is that the function returns void, so we'd need an API change, or a separate checked conversion function
There was a problem hiding this comment.
Good question — I dug into this rather than hand-wave it. Recommendation: merge #154 as-is; keep the silent clamp, and add a checked variant only later as a separate, additive follow-up if a caller actually needs it.
Reasoning:
- Signature/ABI.
hdr_timespec_from_doubleis a publishedvoidsymbol inhdr/hdr_time.h. Changing it to returnintis a source + ABI break that would force a SOVERSION bump — disproportionate for a UBSan fix and against the one-PR-one-purpose norm here. - No caller can consume an error. The only in-tree caller is
scan_start_time(src/hdr_histogram_log.c), which is itselfvoid, and its parent header-scan chain propagates no per-field error. An absentStartTimealready degrades to0viahdr_log_reader_init. So invalid input mapping to0/0lands exactly on the pre-existing "unknown start time" sentinel — consistent behavior, not a new silent failure mode. Even if the function returned an error today, every caller would discard it. - The PR's job is to remove UB.
0/0is defined behavior; the out-of-rangedouble→int cast was UB. Replacing UB with a defined sentinel is the fix. Bolting on error-return machinery is a separate design change (same reason thetv_nsec == 1e9carry and negative-fraction normalization were split into fix: hdr_timespec_from_double could emit a malformed hdr_timespec (tv_nsec == 1e9, or negative) #156).
If a checked path is wanted later, the minimal non-breaking shape is additive and belongs in its own PR, not here:
/* returns 0 on success, -EINVAL on non-finite, -ERANGE on out-of-tv_sec-range input */
int hdr_timespec_from_double_checked(hdr_timespec* t, double value);with the existing void function kept as a thin wrapper that calls it and zeroes on failure — so no caller and no ABI break, and encode/decode callers that care could then propagate the error.
(Adversarial re-review of the current head was 7/7 green through all correctness lenses; this was the only open thread.)
|
Review round: 2026-09-29 (adversarial re-verification). Reviewed commit: 7-lens adversarial pass on the current head — 7/7 GREEN; the finite/range-checked The one open thread — @paulorsousa's question on silently mapping invalid/out-of-range input Verdict: MERGE-READY (merge #154 before #156, or use #156 as the combined fix). |
…istogram#149/HdrHistogram#155/HdrHistogram#157) # Conflicts: # test/hdr_histogram_test.c
# Conflicts: # src/hdr_time.c # test/hdr_histogram_test.c
|
Published the optional checked variant as #161 — |
…variant, follow-up to #154) (#161) * feat: add hdr_timespec_from_double_checked; keep the void form as a wrapper Follow-up to #154/#156. The void hdr_timespec_from_double silently maps invalid or out-of-range input to 0/0 (raised by @paulorsousa on #154). Add an additive checked variant returning 0 / -EINVAL (non-finite) / -ERANGE (doesn't fit tv_sec), zeroing *t on failure, so callers that care (e.g. log decode) can propagate the error. The existing void function becomes a thin wrapper that ignores the return — no behavior or ABI change for current callers. Adds test_timespec_from_double_checked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * hdr_time.h: fold millisecond-accuracy note into the checked variant's doc block Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * log reader: propagate a bad StartTime; checked variant leaves *t unchanged on error hdr_log_read_header now uses hdr_timespec_from_double_checked and returns its error (-EINVAL for non-finite, -ERANGE when it does not fit tv_sec) instead of silently storing zero. The checked variant no longer touches *t on failure; the void wrapper still zeroes it, so existing callers are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: fcostaoliveira <oly@codeperf.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #153, and the second half of what has been failing the weekly
ClusterFuzzLite batch fuzzingworkflow.Problem
hdr_log_read_headerfeeds the#[StartTime: %lffield straight from the file intohdr_timespec_from_double, which converted it with:Converting a
doublethat is out ofintrange — or non-finite — to an integer isundefined behaviour. So any log whose StartTime is outside
intis UB during header parse.A millisecond epoch written where seconds were expected is enough, which is a realistic
mistake rather than a purely hostile input:
The narrow accumulator cascades two more UB sites for such a value: the
(int) round(...)of the resulting huge fraction, and
milliseconds * 1000000. gcc's UBSan reports that lastone first, clang reports the cast — same root cause, and worth knowing when reading the two
CI configurations.
intwas also too narrow regardless of the UB: it truncates every timestamp past 2038 evenon platforms whose
tv_secis 64-bit.Change
Range-check before converting, and widen the result to what
tv_seccan actually hold. Thebound is derived from
sizeof(tv_sec), so this is correct on LP64, LLP64 (wheretv_secisa 32-bit
long) and 32-bit alike —2^(bits-1)is exactly representable as adouble, sothe comparison is exact rather than approximate.
A value
tv_seccannot hold, or a non-finite one, yields a defined0/0instead of a trap.Both fields are always written, so a caller that passes an uninitialized
hdr_timespeccannot read indeterminate values on the reject path — the same concern as the last commit
of #145.
isfiniteis load-bearing here: NaN compares false against both bounds, so withoutit NaN would fall straight through to the cast.
Millisecond resolution is unchanged, as
hdr_time.hdocuments.tv_sec)1403476110.1831403476110/1830000001403476110183.0(ms epoch)1403476110183/01e300,±INFINITY,NAN0/0-2.0-2/0On a 32-bit
tv_secplatform the ms-epoch row rejects to0/0instead, which the testasserts explicitly per
sizeof(tv_sec).Tests
test_timespec_from_doubleinhdr_histogram_test.ccovers a normal value, a negativevalue, a value beyond
intthat a 64-bittv_secholds,±1e300,±INFINITYandNAN.It lives in the always-built suite, so it runs in the
HDR_LOG_REQUIRED=DISABLEDlegs too.handle_wide_start_timereadsregression-log-start-time-overflow.hlogthroughhdr_log_read_header, covering the actual reachable path rather than only the function.log_reader_fuzzerseed corpus automatically via.clusterfuzzlite/build.sh(test/*.hlog), same as the reproducer added in fix: signed-shift overflow in hdr_calculate_bucket_config (UBSan, found by fuzzing) #145.Both tests abort under UBSan on the pre-fix code — reverting just the
src/hdr_time.chunkreproduces the CI message verbatim at
hdr_time.c:92:19, so these are real regression testsrather than assertions that merely happen to pass.
Verification
ctestgcc ASan+UBSan,-fno-sanitize-recover=all(thesanitizersjob's flags)ctestclang ASan+UBSan+float-cast-overflow (the check the fuzzing build uses)ctestRelWithDebInfo, gcc and clangctest-DHDR_LOG_REQUIRED=DISABLED-Wall -Wextra -Wconversion -Wsign-conversionmain(2 pre-existing inhdr_timespec_as_double, none added)MSVC reasoning:
isfinite,truncandldexpare all C99<math.h>, and this file alreadycalls
round/modf, so it does not move the toolchain floor;CHAR_BITandint64_tcomefrom the two added includes.
Combined with #153
Since both PRs fix findings from the same fuzzer, I validated them stacked as well: with
#153 and this change applied together,
ctestis 5/5 under both sanitizer configurationsand 4/4 with logging disabled, and a 600-second
log_reader_fuzzersession(ASan+UBSan+float-cast-overflow, seeded from
test/*.hlog) ran 5.68 M executions withno crash and no UB, at higher coverage than before the fixes. That is the pair that
should take the weekly batch run green.
They touch adjacent lines in
test/CMakeLists.txtandtest/hdr_histogram_log_test.c, sowhichever lands second needs a trivial additive resolution (keep both fixture entries, keep
both
mu_run_testlines) — happy to rebase whenever suits.Steps to reproduce
Not included
hdr_timespec_from_doublecan still round the fraction up to a full second and emittv_nsec = 1000000000(e.g.1.9996givestv_sec=1,tv_nsec=1e9), which is a malformedhdr_timespecbut not UB. It is a normalization bug rather than this defect class, andfixing it changes output for inputs that are currently accepted, so I left it out to keep
this PR to one purpose. Happy to send it separately if you want it.
🤖 Generated with Claude Code