From cbe5d846a9a08ff23eef43dce8280bdf9d7fe80f Mon Sep 17 00:00:00 2001 From: Wondr Date: Fri, 5 Jun 2026 06:59:30 +0100 Subject: [PATCH] printf: avoid formatter panic on large widths --- src/uucore/src/lib/features/format/mod.rs | 19 ++++++++++--- .../src/lib/features/format/num_format.rs | 27 +++++++++++++------ src/uucore/src/lib/features/format/spec.rs | 7 ++--- tests/by-util/test_printf.rs | 23 ++++++++++++++++ 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/uucore/src/lib/features/format/mod.rs b/src/uucore/src/lib/features/format/mod.rs index 66e4d8bdedd..48bc2269713 100644 --- a/src/uucore/src/lib/features/format/mod.rs +++ b/src/uucore/src/lib/features/format/mod.rs @@ -127,10 +127,8 @@ impl Display for FormatError { } } -/// Maximum width for formatting to prevent memory allocation panics. -/// Rust's formatter will panic when trying to allocate memory for very large widths. -/// This limit is somewhat arbitrary but should be well above any practical use case -/// while still preventing formatter panics. +/// Maximum padding width for formatting to prevent pathological output requests. +/// This limit is somewhat arbitrary but should be well above any practical use case. const MAX_FORMAT_WIDTH: usize = 1_000_000; /// Check if a width is too large for formatting. @@ -146,6 +144,19 @@ fn check_width(width: usize) -> std::io::Result<()> { } } +fn write_padding(mut writer: impl Write, byte: u8, len: usize) -> std::io::Result<()> { + check_width(len)?; + + let buffer = [byte; 1024]; + let mut remaining = len; + while remaining > 0 { + let count = remaining.min(buffer.len()); + writer.write_all(&buffer[..count])?; + remaining -= count; + } + Ok(()) +} + /// A single item to format pub enum FormatItem { /// A format specifier diff --git a/src/uucore/src/lib/features/format/num_format.rs b/src/uucore/src/lib/features/format/num_format.rs index a99d7f5f840..55cdc21745e 100644 --- a/src/uucore/src/lib/features/format/num_format.rs +++ b/src/uucore/src/lib/features/format/num_format.rs @@ -724,19 +724,26 @@ fn write_output( // Using min() because self.width could be 0, 0usize - 1usize should be avoided let remaining_width = width - min(width, sign_indicator.len()); - // Check if the width is too large for formatting - super::check_width(remaining_width)?; - match alignment { - NumberAlignment::Left => write!(writer, "{sign_indicator}{s: { + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(s.as_bytes())?; + let padding = remaining_width.saturating_sub(s.len()); + super::write_padding(writer, b' ', padding) + } NumberAlignment::RightSpace => { let is_sign = sign_indicator.starts_with('-') || sign_indicator.starts_with('+'); // When sign_indicator is in ['-', '+'] if is_sign && remaining_width > 0 { // Make sure sign_indicator is just next to number, e.g. "% +5.1f" 1 ==> $ +1.0 - let s = sign_indicator + s.as_str(); - write!(writer, "{s:>width$}", width = remaining_width + 1) // Since we now add sign_indicator and s together, plus 1 + let padding = width.saturating_sub(sign_indicator.len().saturating_add(s.len())); + super::write_padding(&mut writer, b' ', padding)?; + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(s.as_bytes()) } else { - write!(writer, "{sign_indicator}{s:>remaining_width$}") + writer.write_all(sign_indicator.as_bytes())?; + let padding = remaining_width.saturating_sub(s.len()); + super::write_padding(&mut writer, b' ', padding)?; + writer.write_all(s.as_bytes()) } } NumberAlignment::RightZero => { @@ -747,7 +754,11 @@ fn write_output( ("", s.as_str()) }; let remaining_width = remaining_width.saturating_sub(prefix.len()); - write!(writer, "{sign_indicator}{prefix}{rest:0>remaining_width$}") + writer.write_all(sign_indicator.as_bytes())?; + writer.write_all(prefix.as_bytes())?; + let padding = remaining_width.saturating_sub(rest.len()); + super::write_padding(&mut writer, b'0', padding)?; + writer.write_all(rest.as_bytes()) } } } diff --git a/src/uucore/src/lib/features/format/spec.rs b/src/uucore/src/lib/features/format/spec.rs index 3587816b771..4ff5e983261 100644 --- a/src/uucore/src/lib/features/format/spec.rs +++ b/src/uucore/src/lib/features/format/spec.rs @@ -551,14 +551,11 @@ fn write_padded( ) -> Result<(), FormatError> { let padlen = width.saturating_sub(text.len()); - // Check if the padding length is too large for formatting - super::check_width(padlen).map_err(FormatError::IoError)?; - if left { writer.write_all(text)?; - write!(writer, "{: padlen$}", "")?; + super::write_padding(&mut writer, b' ', padlen)?; writer.write_all(text) } .map_err(FormatError::IoError) diff --git a/tests/by-util/test_printf.rs b/tests/by-util/test_printf.rs index 1c8d77c9a1b..ef8f1d2e3e4 100644 --- a/tests/by-util/test_printf.rs +++ b/tests/by-util/test_printf.rs @@ -1485,6 +1485,29 @@ fn test_large_width_format() { } } +#[test] +fn test_large_but_allowed_width_format() { + let width = 111_111; + + let expected_char = format!("{}x", " ".repeat(width - 1)); + new_ucmd!() + .args(&["%111111c", "x"]) + .succeeds() + .stdout_only(expected_char); + + let expected_int = format!("{}1", " ".repeat(width - 1)); + new_ucmd!() + .args(&["%111111.1d", "1"]) + .succeeds() + .stdout_only(expected_int); + + let expected_float = format!("{}1.0", " ".repeat(width - 3)); + new_ucmd!() + .args(&["%111111.1f", "1"]) + .succeeds() + .stdout_only(expected_float); +} + #[test] fn test_extreme_field_width_overflow() { // Test the specific case that was causing panic due to integer overflow