printf: handle extremely large format widths gracefully to fix GNU test panic

This commit is contained in:
Sylvestre Ledru
2025-11-03 23:32:37 +01:00
parent 975e18c032
commit c8663b9112
4 changed files with 51 additions and 3 deletions
+20 -1
View File
@@ -113,7 +113,7 @@ impl Display for FormatError {
Self::InvalidPrecision(precision) => write!(f, "invalid precision: '{precision}'"),
// TODO: Error message below needs some work
Self::WrongSpecType => write!(f, "wrong % directive type was given"),
Self::IoError(_) => write!(f, "io error"),
Self::IoError(_) => write!(f, "write error"),
Self::NoMoreArguments => write!(f, "no more arguments"),
Self::InvalidArgument(_) => write!(f, "invalid argument"),
Self::MissingHex => write!(f, "missing hexadecimal number in escape"),
@@ -127,6 +127,25 @@ 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.
const MAX_FORMAT_WIDTH: usize = 1_000_000;
/// Check if a width is too large for formatting.
/// Returns an error if the width exceeds MAX_FORMAT_WIDTH.
fn check_width(width: usize) -> std::io::Result<()> {
if width > MAX_FORMAT_WIDTH {
Err(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"formatting width too large",
))
} else {
Ok(())
}
}
/// A single item to format
pub enum FormatItem<C: FormatChar> {
/// A format specifier
@@ -693,7 +693,7 @@ fn strip_fractional_zeroes_and_dot(s: &mut String) {
fn write_output(
mut writer: impl Write,
sign_indicator: String,
mut s: String,
s: String,
width: usize,
alignment: NumberAlignment,
) -> std::io::Result<()> {
@@ -706,13 +706,17 @@ fn write_output(
// by storing remaining_width indicating the actual width needed.
// 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:<remaining_width$}"),
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
s = sign_indicator + s.as_str();
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
} else {
write!(writer, "{sign_indicator}{s:>remaining_width$}")
@@ -550,6 +550,10 @@ fn write_padded(
left: bool,
) -> 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$}", "")
+21
View File
@@ -1461,3 +1461,24 @@ fn test_emoji_formatting() {
.succeeds()
.stdout_only("Status: Success 🚀 🎯 Count: 42\n");
}
#[test]
fn test_large_width_format() {
// Test that extremely large width specifications fail gracefully with an error
// rather than panicking. This tests the fix for the printf-surprise.sh GNU test.
// When printf tries to format with a width of 20 million, it should return
// an error message and exit code 1, not panic with exit code 101.
let test_cases = [
("%20000000f", "0"), // float formatting
("%10000000s", "test"), // string formatting
("%15000000d", "42"), // integer formatting
];
for (format, arg) in test_cases {
new_ucmd!()
.args(&[format, arg])
.fails_with_code(1)
.stderr_contains("write error")
.stdout_is("");
}
}