unexpand validates the magnitude of a --tabs=N,+M increment (rejected since #13378 / PR #13383 with tab stop value is too large), but not a plain over-large tabstop value --tabs=N (-t N). A value near usize::MAX, combined with a literal tab in the input (which drives the current column up to the tabstop), reaches write_tabs, where the per-tab-stop arithmetic (scol + nts, scol + 1) is unguarded:
- overflow-checks build: aborts —
attempt to add with overflow at unexpand.rs scol + nts (exit 134).
- default release build (shipped): the add wraps and the output loop then emits spaces up to
col ≈ usize::MAX — an effectively unbounded runaway (multi-GB, never terminates).
GNU unexpand rejects the value up front (unexpand: memory exhausted, exit 1) for both the increment and the plain form.
Steps to reproduce
Overflow-checks build (RUSTFLAGS="-C overflow-checks=on"):
$ printf 'a\tb' | unexpand -t 18446744073709551615
thread 'main' panicked at src/uu/unexpand/src/unexpand.rs:383:26:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)
$ echo $?
134
Default release build — runaway output (never terminates; capped here):
$ printf 'a\tb' | timeout 5 unexpand -t 18446744073709551615 >/dev/null
$ echo $?
124 # still running after 5s; emits unbounded spaces
GNU behavior
$ printf 'a\tb' | /usr/bin/unexpand -t 18446744073709551615
unexpand: memory exhausted
$ echo $?
1
Root cause
write_tabs (the space→tab conversion routine) does unguarded usize arithmetic on the tabstop, and next_tabstop returns a gap nts as large as the tabstop itself:
// src/uu/unexpand/src/unexpand.rs @ 5426f41b
fn write_tabs(...) {
if (ai && ... && print_state.col > print_state.scol + 1) // :378 scol + 1
|| ... {
while let Some(nts) = next_tabstop(tab_config, print_state.scol) {
let target = print_state.scol + nts; // :383 scol + nts <-- overflow
if print_state.col < target {
break;
}
// (wide-blank straddle check) ...
output.write_all(b"\t")?;
print_state.scol = target; // :398
}
}
// Fill the remaining columns.
while print_state.col > print_state.scol { // :405 runaway in release:
// ...
output.write_all(b" ")?; // :419 emits ~col spaces
print_state.scol += 1; // :420
}
...
}
unexpandvalidates the magnitude of a--tabs=N,+Mincrement (rejected since #13378 / PR #13383 withtab stop value is too large), but not a plain over-large tabstop value--tabs=N(-t N). A value nearusize::MAX, combined with a literal tab in the input (which drives the current column up to the tabstop), reacheswrite_tabs, where the per-tab-stop arithmetic (scol + nts,scol + 1) is unguarded:attempt to add with overflowatunexpand.rsscol + nts(exit 134).col ≈ usize::MAX— an effectively unbounded runaway (multi-GB, never terminates).GNU
unexpandrejects the value up front (unexpand: memory exhausted, exit 1) for both the increment and the plain form.Steps to reproduce
Overflow-checks build (
RUSTFLAGS="-C overflow-checks=on"):Default release build — runaway output (never terminates; capped here):
GNU behavior
Root cause
write_tabs(the space→tab conversion routine) does unguardedusizearithmetic on the tabstop, andnext_tabstopreturns a gapntsas large as the tabstop itself: