unexpand: a plain over-large `--tabs`/`-t` value overflows (overflow-checks) or runs away (release) in the tab-conversion output path
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 24.1k
- Forks
- 2k
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 365
Description
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 overflowatunexpand.rsscol + 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
}
...
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/uu/unexpand/src/unexpand.rs, especially write_tabs and next_tabstop, and reproduce the overflow-checks and release-build cases from the issue. Trace validation of plain --tabs=N values and the per-tab-stop arithmetic; done means the oversized value is rejected safely without a panic or runaway output, matching the stated GNU behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100