diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index b275401ece7..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; @@ -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..34d8a5811c6 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -782,6 +782,21 @@ fn test_parse_error_hex() { .usage_error("invalid floating point argument: '0xlmnop'"); } +#[test] +fn test_format_a_huge_negative_exponent_does_not_oom() { + // 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"); +} + #[test] fn test_format_option() { new_ucmd!()