Skip to content

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

Description

@leeewee

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions