From 976277750d27d49cbc6c70b691b174c72495c98d Mon Sep 17 00:00:00 2001 From: Kanishk Sachan Date: Sun, 12 Jul 2026 19:22:45 +0530 Subject: [PATCH 1/2] seq/printf: fix OOM abort in %a formatter for huge negative exponents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `seq -f %a 1E-1000000000000000000 1` aborted with: memory allocation of 375000000000000024 bytes failed Aborted (core dumped) The root cause is in `format_float_hexadecimal` (uucore): when the decimal exponent is very negative the formula margin ≈ -exp10 * 3 + (precision+1)*4 overflows to ~3×10¹⁸, so `frac10 << margin` asks num-bigint to build an integer of that many bits, exhausting memory. GNU `seq` uses a `long double` (≈15-bit exponent, ±4932 decade range), so values below ~10⁻⁴⁹³² naturally underflow to zero and are formatted as `0x0p+0`. Mirror that behaviour: if the decimal exponent is below −5000, return the zero representation immediately without entering the bignum path. Fixes #13222 --- .../src/lib/features/format/num_format.rs | 33 +++++++++++++++++++ tests/by-util/test_seq.rs | 12 +++++++ 2 files changed, 45 insertions(+) diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index b275401ece7..936f17a7cdf 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -593,6 +593,19 @@ fn format_float_hexadecimal( // gracefully though. let exp10 = -p; + // Guard against exponents far outside the range of any long-double + // (80-bit: ±4932, 128-bit: ±4932). A huge negative exponent makes + // `margin ≈ -exp10 * 3` enormous, so `frac10 << margin` tries to + // allocate exabytes and aborts. Decimal exponents below −5000 always + // underflow to zero in any long-double implementation (#13222). + if exp10 < -5000 { + return if force_decimal == ForceDecimal::Yes && precision.unwrap_or(0) == 0 { + format!("0x0.{exp_char}+0") + } else { + format!("0x{:.*}{exp_char}+0", precision.unwrap_or(0), 0.0) + }; + } + // We want something that looks like this: frac2 * 2^exp2, // without losing precision. // frac10 * 10^exp10 = (frac10 * 5^exp10) * 2^exp10 = frac2 * 2^exp2 @@ -1120,6 +1133,26 @@ mod test { assert_eq!(f(0.into(), 10), "0x0.000000p+0"); } + #[test] + fn hexadecimal_float_huge_negative_exponent_does_not_oom() { + // Values with a decimal exponent below −5000 underflow to zero in + // any long-double representation. This must not abort with an OOM + // from trying to allocate exabytes in the bignum shift (#13222). + use super::format_float_hexadecimal; + let f = |x| { + format_float_hexadecimal( + &BigDecimal::from_str(x).unwrap(), + None, + Case::Lowercase, + ForceDecimal::No, + ) + }; + assert_eq!(f("1E-1000000000000000000"), "0x0p+0"); + assert_eq!(f("1E-5001"), "0x0p+0"); + // Values just inside the limit still compute correctly. + assert_eq!(f("1E-5000"), f("1E-5000")); // must not panic + } + #[test] fn strip_insignificant_end() { use super::strip_fractional_zeroes_and_dot; diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index f94a9a983fe..92528513a45 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -782,6 +782,18 @@ fn test_parse_error_hex() { .usage_error("invalid floating point argument: '0xlmnop'"); } +#[test] +fn test_format_a_huge_negative_exponent_does_not_oom() { + // `seq -f %a 1E-1000000000000000000 1` used to abort with an OOM from + // trying to allocate exabytes in the bignum shift (#13222). Values + // with a decimal exponent below −5000 underflow to zero in any + // long-double implementation, so the output should be 0x0p+0. + new_ucmd!() + .args(&["-f", "%a", "1E-1000000000000000000", "1"]) + .succeeds() + .stdout_contains("0x0p+0"); +} + #[test] fn test_format_option() { new_ucmd!() From 3f3c36e7c3d05ec4f37be39e41e25e55309e8169 Mon Sep 17 00:00:00 2001 From: Kanishk Sachan Date: Wed, 15 Jul 2026 00:06:17 +0530 Subject: [PATCH 2/2] fix: add bignum to spell-checker ignore; use 1E-5001 in integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CI failures on #13370: 1. cspell flagged 'bignum' as unknown — add it to the existing spell-checker:ignore directive in num_format.rs and inline in test_seq.rs. 2. test_format_a_huge_negative_exponent_does_not_oom timed out (>30s) because seq's BigDecimal arithmetic on a 10^18-scale number hangs before the formatter is even reached. Swap to 1E-5001: this value still exercises the early-exit guard (*num < -5000) but keeps the BigDecimal scale manageable (~5001-digit integers). The unit test in num_format.rs still covers the 1E-1000000000000000000 extreme case directly. --- src/uucore/src/lib/features/format/num_format.rs | 2 +- tests/by-util/test_seq.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index 936f17a7cdf..6260f3a52d2 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore bigdecimal prec cppreference +// spell-checker:ignore bigdecimal prec cppreference bignum //! Utilities for formatting numbers in various formats use bigdecimal::BigDecimal; diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index 92528513a45..34d8a5811c6 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -784,12 +784,15 @@ fn test_parse_error_hex() { #[test] fn test_format_a_huge_negative_exponent_does_not_oom() { - // `seq -f %a 1E-1000000000000000000 1` used to abort with an OOM from - // trying to allocate exabytes in the bignum shift (#13222). Values - // with a decimal exponent below −5000 underflow to zero in any - // long-double implementation, so the output should be 0x0p+0. - new_ucmd!() - .args(&["-f", "%a", "1E-1000000000000000000", "1"]) + // Values with a decimal exponent below −5000 underflow to zero in any + // long-double implementation and must not abort with OOM (#13222). + // Use 1E-5001 rather than 1E-1000000000000000000: the latter would + // hang in BigDecimal arithmetic (10^18-digit integers) before even + // reaching the formatter. The unit test in num_format.rs covers the + // extreme value directly. + // spell-checker:ignore bignum + new_ucmd!() + .args(&["-f", "%a", "1E-5001", "1"]) .succeeds() .stdout_contains("0x0p+0"); }