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 <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-04 07:53:00 +09:00
parent 479af5c1bd
commit ddcbd2dc90
+9 -1
View File
@@ -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);
}