Skip to content

Commit e84b261

Browse files
authored
Merge pull request #9133 from sylvestre/printf-surprise
printf: handle extremely large format widths gracefully to fix GNU test panic
2 parents c7f96cb + c8663b9 commit e84b261

File tree

4 files changed

+51
-3
lines changed

4 files changed

+51
-3
lines changed

src/uucore/src/lib/features/format/mod.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ impl Display for FormatError {
113113
Self::InvalidPrecision(precision) => write!(f, "invalid precision: '{precision}'"),
114114
// TODO: Error message below needs some work
115115
Self::WrongSpecType => write!(f, "wrong % directive type was given"),
116-
Self::IoError(_) => write!(f, "io error"),
116+
Self::IoError(_) => write!(f, "write error"),
117117
Self::NoMoreArguments => write!(f, "no more arguments"),
118118
Self::InvalidArgument(_) => write!(f, "invalid argument"),
119119
Self::MissingHex => write!(f, "missing hexadecimal number in escape"),
@@ -127,6 +127,25 @@ impl Display for FormatError {
127127
}
128128
}
129129

130+
/// Maximum width for formatting to prevent memory allocation panics.
131+
/// Rust's formatter will panic when trying to allocate memory for very large widths.
132+
/// This limit is somewhat arbitrary but should be well above any practical use case
133+
/// while still preventing formatter panics.
134+
const MAX_FORMAT_WIDTH: usize = 1_000_000;
135+
136+
/// Check if a width is too large for formatting.
137+
/// Returns an error if the width exceeds MAX_FORMAT_WIDTH.
138+
fn check_width(width: usize) -> std::io::Result<()> {
139+
if width > MAX_FORMAT_WIDTH {
140+
Err(std::io::Error::new(
141+
std::io::ErrorKind::OutOfMemory,
142+
"formatting width too large",
143+
))
144+
} else {
145+
Ok(())
146+
}
147+
}
148+
130149
/// A single item to format
131150
pub enum FormatItem<C: FormatChar> {
132151
/// A format specifier

src/uucore/src/lib/features/format/num_format.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ fn strip_fractional_zeroes_and_dot(s: &mut String) {
693693
fn write_output(
694694
mut writer: impl Write,
695695
sign_indicator: String,
696-
mut s: String,
696+
s: String,
697697
width: usize,
698698
alignment: NumberAlignment,
699699
) -> std::io::Result<()> {
@@ -706,13 +706,17 @@ fn write_output(
706706
// by storing remaining_width indicating the actual width needed.
707707
// Using min() because self.width could be 0, 0usize - 1usize should be avoided
708708
let remaining_width = width - min(width, sign_indicator.len());
709+
710+
// Check if the width is too large for formatting
711+
super::check_width(remaining_width)?;
712+
709713
match alignment {
710714
NumberAlignment::Left => write!(writer, "{sign_indicator}{s:<remaining_width$}"),
711715
NumberAlignment::RightSpace => {
712716
let is_sign = sign_indicator.starts_with('-') || sign_indicator.starts_with('+'); // When sign_indicator is in ['-', '+']
713717
if is_sign && remaining_width > 0 {
714718
// Make sure sign_indicator is just next to number, e.g. "% +5.1f" 1 ==> $ +1.0
715-
s = sign_indicator + s.as_str();
719+
let s = sign_indicator + s.as_str();
716720
write!(writer, "{s:>width$}", width = remaining_width + 1) // Since we now add sign_indicator and s together, plus 1
717721
} else {
718722
write!(writer, "{sign_indicator}{s:>remaining_width$}")

src/uucore/src/lib/features/format/spec.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,10 @@ fn write_padded(
550550
left: bool,
551551
) -> Result<(), FormatError> {
552552
let padlen = width.saturating_sub(text.len());
553+
554+
// Check if the padding length is too large for formatting
555+
super::check_width(padlen).map_err(FormatError::IoError)?;
556+
553557
if left {
554558
writer.write_all(text)?;
555559
write!(writer, "{: <padlen$}", "")

tests/by-util/test_printf.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,3 +1461,24 @@ fn test_emoji_formatting() {
14611461
.succeeds()
14621462
.stdout_only("Status: Success 🚀 🎯 Count: 42\n");
14631463
}
1464+
1465+
#[test]
1466+
fn test_large_width_format() {
1467+
// Test that extremely large width specifications fail gracefully with an error
1468+
// rather than panicking. This tests the fix for the printf-surprise.sh GNU test.
1469+
// When printf tries to format with a width of 20 million, it should return
1470+
// an error message and exit code 1, not panic with exit code 101.
1471+
let test_cases = [
1472+
("%20000000f", "0"), // float formatting
1473+
("%10000000s", "test"), // string formatting
1474+
("%15000000d", "42"), // integer formatting
1475+
];
1476+
1477+
for (format, arg) in test_cases {
1478+
new_ucmd!()
1479+
.args(&[format, arg])
1480+
.fails_with_code(1)
1481+
.stderr_contains("write error")
1482+
.stdout_is("");
1483+
}
1484+
}

0 commit comments

Comments
 (0)