Skip to content

Decision: How the Guard Reads a Shell-Rewritable Word in a timeout Run (#2117) #2118

Closed

Description

@ptr727

Question

Pull request #2117 (handoff #2072, track auto-1634, branch feature/auto-1634 at cd97e26) fixes #1634. Decision #2079 said to open it and let the review loop dispose of that pass's findings. The pass raised one finding, fixed in 62ac767. The next pass raised 3, fixed in cd97e26. The pass after that raised 3 more, all introduced. Both edit rounds for this push are spent, so editing stopped. How should the guard read a word the shell may rewrite?

What the branch holds now

  • A timeout sending signal 0 is no bound unless a duration-form -k follows it.
  • An option word or option value the shell may rewrite (-${F}0, -k $K, -s $SIG, a brace, a glob, a tilde) is read as both a signal-0 -s and a -k of no duration form. A later literal -s or -k replaces its own half.
  • A nested timeout chain is bounded only when every outer one sends signal 0 with no -k and the innermost one is a bound. Past the prefixes and assignments directly after an outer duration, a word naming timeout, or one the shell may rewrite, is read as nesting.
  • --selftest passes, each new row was checked by reverting its logic, and every local gate is clean.

Open findings from the last strict pass

introduced, 3. Each is an allow, and each was run against GNU timeout 9.7 with the loop outliving the outer bound.

  1. An assignment argument is skipped unread. timeout -s KILL 10 env X=$T 800 bash -c '<the loop>' with T='1 timeout' is allowed. The prefix skip passes X=$T before the rewrite check sees it, and after env the shell word-splits it, so an inner timeout runs.
  2. A launcher builds the inner timeout from its own arguments. timeout -s KILL 10 env -S 'timeout 800' bash -c '<the loop>' and echo timeout | timeout -s KILL 10 xargs -I% % 800 bash -c '<the loop>' are allowed. No whole word names timeout.
  3. "A later -s or -k replaces its own half" is false. A rewritten word can split into a duration and a command, which ends option parsing, so the later literal -k becomes that command's argument. timeout -$X -k 30 900 bash -c '<the loop>' is allowed, with X holding s0 900 bash -c '<shifts>'. The self-test row allowing timeout -${F}0 -k 30 900 ... rests on the same false premise.

Each pass on this lane has found new holes in the rewrite model itself, and finding 3 is against text the last round wrote.

Options

  1. Recommended: deny any rewritable word in a timeout run. Any word between timeout and the wrapper that the shell may rewrite makes the run no bound, whatever follows it. That covers the options, the values, the prefix span, and assignment arguments. No later -s or -k rescues it. This closes findings 1 and 3 by construction instead of modelling what the rewrite yields. Finding 2 is a different class, a launcher building a command from its arguments. It would be declared in README requirement 7 as a shape not reached and filed as a follow-up. The cost is a declared false deny on a rare shape such as timeout 900 env PATH=$HOME/bin bash -c '<the loop>'.
  2. Keep the current model and declare all three as limits. Remove the false sentence and the false self-test row, then list the three shapes in README requirement 7 as not reached. The PR review loop takes it from there. This is the smallest diff, but the rewrite model stays one that each pass finds new holes in.
  3. Fix the three within the current model. Read assignment arguments, env -S, and xargs, and drop the "later option replaces" rule. The lane parks again if the next pass finds anything introduced, which every pass on this lane has so far.

Blocks

Activity

  1. added
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Sep 30, 2026
  2. ptr727 commented on Sep 30, 2026

    @ptr727
    OwnerAuthor

    Answered by the maintainer on 2026-09-30: option 1: deny any rewritable word in a timeout run, whatever follows it. Declare the launcher class (finding 2) as a shape not reached in README requirement 7, and file it as a follow-up.

  3. removed
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions