diff --git a/src/uu/seq/src/numberparse.rs b/src/uu/seq/src/numberparse.rs index ba4ab4e4eba..d30b1e827e5 100644 --- a/src/uu/seq/src/numberparse.rs +++ b/src/uu/seq/src/numberparse.rs @@ -373,8 +373,9 @@ mod tests { #[test] fn test_parse_max_exponents() { - // Make sure exponents much bigger than i64::MAX cause errors - assert!("1e9223372036854775807".parse::().is_ok()); + // Exponents at or beyond i64::MAX cause errors, matching GNU seq + // (e.g. `seq -w 1e9223372036854775807 1` fails on GNU coreutils too). + assert!("1e9223372036854775807".parse::().is_err()); assert!("1e92233720368547758070".parse::().is_err()); } } diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index 3860f923bb4..f718b22087b 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -165,15 +165,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let padding = if options.equal_width { let precision_value = precision.unwrap_or(0); + // Saturate rather than overflow: a value with an astronomically large + // exponent makes `num_integral_digits` near `usize::MAX`. The resulting + // width is rejected later by the formatter's width check. first .num_integral_digits .max(increment.num_integral_digits) .max(last.num_integral_digits) - + if precision_value > 0 { - precision_value + 1 + .saturating_add(if precision_value > 0 { + precision_value.saturating_add(1) } else { 0 - } + }) } else { 0 }; diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index 6e61ec92169..1f1c79de556 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -146,6 +146,18 @@ fn check_width(width: usize) -> std::io::Result<()> { } } +/// Reject a precision larger than printf/C allows (`i32::MAX`). +/// +/// A precision near `usize::MAX` would otherwise overflow the precision/exponent +/// arithmetic in the float formatters, so we cap it the way C `printf` does. +pub(crate) fn check_precision(precision: usize) -> Result<(), FormatError> { + if precision > i32::MAX as usize { + Err(FormatError::InvalidPrecision(precision.to_string())) + } else { + Ok(()) + } +} + /// A single item to format pub enum FormatItem { /// A format specifier @@ -410,3 +422,29 @@ impl, T> Format { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{FormatError, check_precision}; + + #[test] + fn check_precision_caps_at_i32_max() { + // Anything up to i32::MAX is accepted. + assert!(check_precision(0).is_ok()); + assert!(check_precision(42).is_ok()); + assert!(check_precision(i32::MAX as usize).is_ok()); + + // Anything above i32::MAX is rejected, reporting the offending value. + let precision = i32::MAX as usize + 1; + match check_precision(precision) { + Err(FormatError::InvalidPrecision(reported)) => { + assert_eq!(reported, precision.to_string()); + } + other => panic!("expected InvalidPrecision, got {other:?}"), + } + assert!(matches!( + check_precision(usize::MAX), + Err(FormatError::InvalidPrecision(_)) + )); + } +} diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index b275401ece7..3d17d108304 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 bignums //! Utilities for formatting numbers in various formats use bigdecimal::BigDecimal; @@ -13,7 +13,7 @@ use std::cmp::min; use std::io::Write; use super::{ - ExtendedBigDecimal, FormatError, + ExtendedBigDecimal, FormatError, check_precision, spec::{CanAsterisk, Spec}, }; @@ -257,6 +257,29 @@ impl Formatter<&ExtendedBigDecimal> for Float { let mut alignment = self.alignment; + // The formatters below do arithmetic on the decimal exponent (negating it, + // scaling by the precision, shifting bignums by it), which would overflow + // for a scale outside `i32`. The parser already rejects such magnitudes as + // overflow/underflow, so this only guards values built without going + // through it; treat them the same way a real float would (as +-inf/0) + // rather than erroring. + if let ExtendedBigDecimal::BigDecimal(bd) = &abs { + if i32::try_from(bd.fractional_digit_count()).is_err() { + let overflow = bd.fractional_digit_count().is_negative(); + let abs = if overflow { + ExtendedBigDecimal::Infinity + } else { + ExtendedBigDecimal::zero() + }; + if alignment == NumberAlignment::RightZero { + alignment = NumberAlignment::RightSpace; + } + let s = format_float_non_finite(&abs, self.case); + let sign_indicator = get_sign_indicator(self.positive_sign, negative); + return write_output(writer, sign_indicator, s, self.width, alignment); + } + } + let s = if let ExtendedBigDecimal::BigDecimal(bd) = abs { match self.variant { FloatVariant::Decimal => { @@ -314,6 +337,10 @@ impl Formatter<&ExtendedBigDecimal> for Float { Some(CanAsterisk::Asterisk(_)) => return Err(FormatError::WrongSpecType), }; + if let Some(precision) = precision { + check_precision(precision)?; + } + Ok(Self { variant, case, @@ -618,7 +645,7 @@ fn format_float_hexadecimal( // (since 5^-exp10 < 8^-exp10), so we add that, and another bit for // rounding. let margin = - ((max_precision + 1) as i64 * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1; + ((max_precision as i64 + 1) * 4 - frac10.bits() as i64).max(0) + -exp10 * 3 + 1; // frac10 * 10^exp10 = frac10 * 2^margin * 10^exp10 * 2^-margin = // (frac10 * 2^margin * 5^exp10) * 2^exp10 * 2^-margin = @@ -631,7 +658,7 @@ fn format_float_hexadecimal( // Emulate x86(-64) behavior, we display 4 binary digits before the decimal point, // so the value will always be between 0x8 and 0xf. - let wanted_bits = (BEFORE_BITS + max_precision * 4) as u64; + let wanted_bits = BEFORE_BITS as u64 + max_precision as u64 * 4; let bits = frac2.bits(); exp2 += bits as i64 - wanted_bits as i64; @@ -661,7 +688,7 @@ fn format_float_hexadecimal( digits.make_ascii_uppercase(); } let (first_digit, remaining_digits) = digits.split_at(1); - let exponent = exp2 + (4 * max_precision) as i64; + let exponent = exp2 + 4 * max_precision as i64; let mut remaining_digits = remaining_digits.to_string(); if precision.is_none() { diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index c357548a7f0..8cd0736ec27 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (vars) intmax ptrdiff padlen use super::{ - ExtendedBigDecimal, FormatChar, FormatError, OctalParsing, + ExtendedBigDecimal, FormatChar, FormatError, OctalParsing, check_precision, num_format::{ self, Case, FloatVariant, ForceDecimal, Formatter, NumberAlignment, PositiveSign, Prefix, UnsignedIntVariant, @@ -417,9 +417,7 @@ impl Spec { let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default(); let i = args.next_i64(*position); - if precision as u64 > i32::MAX as u64 { - return Err(FormatError::InvalidPrecision(precision.to_string())); - } + check_precision(precision)?; num_format::SignedInt { width, @@ -445,9 +443,7 @@ impl Spec { let precision = resolve_asterisk_precision(*precision, args).unwrap_or_default(); let i = args.next_u64(*position); - if precision as u64 > i32::MAX as u64 { - return Err(FormatError::InvalidPrecision(precision.to_string())); - } + check_precision(precision)?; num_format::UnsignedInt { variant: *variant, @@ -476,10 +472,8 @@ impl Spec { let precision = resolve_asterisk_precision(*precision, args); let f: ExtendedBigDecimal = args.next_extended_big_decimal(*position); - if precision.is_some_and(|p| p as u64 > i32::MAX as u64) { - return Err(FormatError::InvalidPrecision( - precision.unwrap().to_string(), - )); + if let Some(precision) = precision { + check_precision(precision)?; } num_format::Float { diff --git a/src/uucore/src/lib/features/parser/num_parser.rs b/src/uucore/src/lib/features/parser/num_parser.rs index 952c04edc01..951d23ba257 100644 --- a/src/uucore/src/lib/features/parser/num_parser.rs +++ b/src/uucore/src/lib/features/parser/num_parser.rs @@ -425,10 +425,14 @@ fn construct_extended_big_decimal( } else { let new_scale = -exponent + scale; - // BigDecimal "only" supports i64 scale. + // BigDecimal supports an i64 scale, but a magnitude anywhere near that + // range is already many orders beyond any real floating-point type (a + // long double tops out around 10^±4932), and formatting it would mean + // materializing quintillions of digits. Cap the scale at `i32` so such + // values are treated as overflow/underflow, like a real float would. // Note that new_scale is a negative exponent: large positive value causes an underflow, large negative values an overflow. - if let Some(new_scale) = new_scale.to_i64() { - BigDecimal::from_bigint(signed_digits, new_scale) + if let Some(new_scale) = new_scale.to_i32() { + BigDecimal::from_bigint(signed_digits, new_scale.into()) } else { return Err(make_error(new_scale.is_negative(), negative)); } diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 1c8d77c9a1b..989afa12605 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -801,6 +801,35 @@ fn test_overflow() { .stderr_is("printf: '36893488147419103232': Numerical result out of range\n"); } +#[test] +fn test_extreme_exponent_does_not_overflow() { + // A value whose decimal exponent does not fit in an i32 used to overflow the + // exponent arithmetic in the float formatters (panic in debug, wrong value or + // bad allocation in release). GNU printf treats such a magnitude the same + // way it would treat any other out-of-range float: it reports the overflow + // on stderr but still prints inf (or 0, on underflow) to stdout. + for spec in ["%a", "%e", "%g", "%f"] { + new_ucmd!() + .args(&[spec, "5e8123456789012345678"]) + .fails_with_code(1) + .stderr_contains("Numerical result out of range") + .stdout_contains("inf"); + + let zero = match spec { + "%a" => "0x0p+0", + "%e" => "0.000000e+00", + "%g" => "0", + "%f" => "0.000000", + _ => unreachable!(), + }; + new_ucmd!() + .args(&[spec, "7E-8123456789012345678"]) + .fails_with_code(1) + .stderr_contains("Numerical result out of range") + .stdout_is(zero); + } +} + #[test] fn partial_char() { new_ucmd!() diff --git a/tests/by-util/test_seq.rs b/tests/by-util/test_seq.rs index f94a9a983fe..5e749e07236 100644 --- a/tests/by-util/test_seq.rs +++ b/tests/by-util/test_seq.rs @@ -30,6 +30,49 @@ fn test_format_and_equal_width() { .stderr_contains("format string may not be specified"); } +#[test] +fn test_format_precision_too_large() { + // A precision near usize::MAX used to overflow the precision arithmetic + // shared with the float formatters; it must be rejected as invalid. + for spec in ["%.18446744073709551615e", "%.9999999999999999999a"] { + new_ucmd!() + .args(&[format!("--format={spec}"), "1".to_string()]) + .fails_with_code(1) + .no_stdout() + .stderr_contains("invalid precision"); + } +} + +#[test] +fn test_format_extreme_exponent_does_not_overflow() { + // A value with an exponent that does not fit in an i32 used to overflow the + // hex-float exponent math. GNU seq rejects such astronomically large + // magnitudes outright (unlike printf, which tolerates them as inf/0), so it + // must now fail cleanly with a parse error instead of panicking. + new_ucmd!() + .args(&[ + "--format=%a", + "5e8123456789012345678", + "5e8123456789012345678", + ]) + .fails_with_code(1) + .no_stdout() + .usage_error("invalid floating point argument: '5e8123456789012345678'"); +} + +#[test] +fn test_equal_width_huge_exponent_does_not_overflow() { + // Equal-width padding adds the integral-digit count to the precision; a value + // with an astronomically large exponent used to overflow that arithmetic. + // GNU seq rejects such magnitudes as an invalid argument rather than + // accepting them, so it must fail cleanly instead of panicking or succeeding. + new_ucmd!() + .args(&["-w", "1e9223372036854775807", "1e-9223372036854775807", "1"]) + .fails_with_code(1) + .no_stdout() + .usage_error("invalid floating point argument: '1e9223372036854775807'"); +} + #[test] fn test_hex_rejects_sign_after_identifier() { new_ucmd!()