uutils / uutils/coreutils

unexpand: a plain over-large `--tabs`/`-t` value overflows (overflow-checks) or runs away (release) in the tab-conversion output path

Open
#14,378 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

U - unexpand
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 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
    }
    ...
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.