Skip to content

Commit afbc154

Browse files
committed
fix(printf): compute asterisk width magnitude in unsigned arithmetic
A negative '*' field width argument of i64::MIN used to panic with attempt to negate with overflow because |i64::MIN| = 2^63 is not representable in i64/isize. Use nb.unsigned_abs() to compute the magnitude in unsigned arithmetic, which can represent 2^63. Add a unit test for the width resolution and an end-to-end regression test verifying printf fails gracefully with a write error instead of panicking. Fixes #13766
1 parent ec7e986 commit afbc154

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,8 @@ fn resolve_asterisk_width(
511511
Some(CanAsterisk::Asterisk(loc)) => {
512512
let nb = args.next_i64(loc);
513513
if nb < 0 {
514-
Some((usize::try_from(-(nb as isize)).ok().unwrap_or(0), true))
514+
// Unsigned arithmetic, so `i64::MIN` (magnitude 2^63) doesn't overflow.
515+
Some((usize::try_from(nb.unsigned_abs()).ok().unwrap_or(0), true))
515516
} else {
516517
Some((usize::try_from(nb).ok().unwrap_or(0), false))
517518
}
@@ -670,6 +671,26 @@ mod tests {
670671
)
671672
);
672673
}
674+
675+
#[test]
676+
fn asterisk_i64_min_width() {
677+
// Regression test for https://github.com/uutils/coreutils/issues/13766
678+
// |i64::MIN| = 2^63 overflows i64, so the magnitude of a negative
679+
// `*` width must be computed in unsigned arithmetic.
680+
let expected = usize::try_from(i64::MIN.unsigned_abs()).unwrap_or(0);
681+
for arg in [
682+
FormatArgument::SignedInt(i64::MIN),
683+
FormatArgument::Unparsed(i64::MIN.to_string().into()),
684+
] {
685+
assert_eq!(
686+
Some((expected, true)),
687+
resolve_asterisk_width(
688+
Some(CanAsterisk::Asterisk(ArgumentLocation::NextArgument)),
689+
&mut FormatArguments::new(&[arg]),
690+
)
691+
);
692+
}
693+
}
673694
}
674695

675696
mod resolve_asterisk_precision {

tests/by-util/test_printf.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,6 +1538,22 @@ fn test_extreme_field_width_overflow() {
15381538
.stderr_contains("printf: write error"); //could contains additional message like "formatting width too large" not in GNU, thats fine.
15391539
}
15401540

1541+
#[test]
1542+
fn test_asterisk_width_i64_min_no_panic() {
1543+
// Regression test for https://github.com/uutils/coreutils/issues/13766
1544+
// An `i64::MIN` '*' width used to panic with "attempt to negate with overflow".
1545+
// It must not panic: on 64-bit it fails with a write error, on 32-bit the
1546+
// width is clamped to 0 and printf succeeds.
1547+
let result = new_ucmd!()
1548+
.args(&["|%*d|", &i64::MIN.to_string(), "1"])
1549+
.run();
1550+
assert!(
1551+
result.succeeded() || result.code() == 1,
1552+
"printf must not panic on an i64::MIN '*' width (got exit code {})",
1553+
result.code()
1554+
);
1555+
}
1556+
15411557
#[test]
15421558
fn test_q_string_control_chars_with_quotes() {
15431559
// Test %q with control characters and single quotes combined.

0 commit comments

Comments
 (0)