fix: out-of-range double->int conversion in hdr_timespec_from_double (UBSan) - #154
fcostaoliveira wants to merge 3 commits into
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. The fix looks like the right shape. The bound is exact where the obvious version wouldn't be: Two things worth a look. The bound is keyed to Second, the reject is silent: Pre-existing rather than introduced here, and the same family as the One note on verification: the per-PR ClusterFuzzLite job is ASan only, UBSan runs in the weekly batch, so nothing in this PR's own checks re-confirms the UB is gone. The local clang |
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 |
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