From ddcbd2dc90fbc7ac065865be90d0cf7145cafb44 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Mon, 4 May 2026 07:53:00 +0900 Subject: [PATCH] fp_uscale.c: fix shift and clz UB in tiny-float formatting Two UBSan issues exposed by sprintf("%f", 1e-7) and similar: 1. uscale() shifted hi by c.s without bounding c.s, hitting UB when c.s >= 64. The mask line had `c.s & 63`, but the actual `hi >> c.s` line did not, so the partial guard was incomplete. On x86 the hardware silently masks the shift, producing wrong output ("1844674407370.955078" for 1e-7) instead of crashing. When c.s >= 64 the value rounds to 0 with sticky=1, so we can bail early. 2. count_digits(0) called bits_len64(0) -> clz64(0), which is UB. The only other bits_len64 caller already guards d == 0; align count_digits with that pattern. Returning 1 (since "0" is one digit) preserves output formatting. Reported by OSS-Fuzz (clusterfuzz testcase 5210395240628224). Co-authored-by: Claude --- src/fp_uscale.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/fp_uscale.c b/src/fp_uscale.c index 071f71c9f..b01de8fc2 100644 --- a/src/fp_uscale.c +++ b/src/fp_uscale.c @@ -818,7 +818,14 @@ static unrounded uscale(uint64_t x, scaler c) mul64(x, c.pm.hi, &hi, &mid); uint64_t sticky = 1; - uint64_t mask = (1ULL << (c.s & 63)) - 1; + + if (c.s >= 64) { + /* x * 10^p < 2^(c.s-64), i.e. < 1 in the unrounded "1.0 = 4" encoding; + rounds to 0 with sticky=1 */ + return sticky; + } + + uint64_t mask = (1ULL << c.s) - 1; if ((hi & mask) == 0) { mul64(x, c.pm.lo, &mid2, &lo_unused); @@ -886,6 +893,7 @@ static const uint64_t uint64_pow10[20] = { static int count_digits(uint64_t d) { + if (d == 0) return 1; /* clz64(0) is UB; "0" is one digit */ int nd = log10_pow2(bits_len64(d)); return nd + (d >= uint64_pow10[nd] ? 1 : 0); }