Skip to content

FIX: Make ShellCommandOutputScorer destructive-shell regex linear time - #3702

Open
Muhammad Waqar Anwar (WaqarAnwar) wants to merge 1 commit into
microsoft:mainfrom
WaqarAnwar:fix/shell-scorer-regex-backtracking
Open

Muhammad Waqar Anwar (WaqarAnwar) wants to merge 1 commit into
microsoft:mainfrom
WaqarAnwar:fix/shell-scorer-regex-backtracking

Conversation

@WaqarAnwar

Copy link
Copy Markdown

Description

Fixes #3698.

The Destructive Shell pattern in ShellCommandOutputScorer (added in #3597) allowed any repeated flag to act as the recursive/force flag, with unbounded argument repetition on both sides. On long nonmatching input the engine retried every split. Because RegexScorer runs pattern.search synchronously inside its async method, this also blocked the event loop.

It is worse than quadratic when rm -rf itself repeats, since each rm restarts the search:

Input Before After
"rm " + "-rf " * 1000 + "relative" (issue repro) 0.150 s 0.0002 s
"rm -rf " * 500 + "relative" 3.6 s 0.0002 s
"rm -rf " * 1000 + "relative" 29 s 0.0004 s
"rm -rf " * 8000 + "relative" does not finish 0.004 s

Fix

The rm branch now:

  1. starts only at the beginning of a command segment (text between ;, |, &, >, <),
  2. commits, using atomic groups (Python 3.11+, already required), to the first rm and the first recursive/force flag after it,
  3. then lazily searches for a protected target (/ other than /tmp, ~, $HOME, ${HOME}).

If any rm … flag … target sequence exists in a segment, then the first rm followed by its first flag also precedes that target. So the result is the same, but each segment is scanned in linear time. The flag must now be a whole token ((?=\s)). This is what the old pattern effectively required too, and committing to the first flag would otherwise break on tokens like -rf.x.

Separator behavior is unchanged (rm -rf build; cd /var still does not match).

Tests and Documentation

  • I compared the old and new patterns on 200,000 random command strings built from rm, flags, targets, separators and rm-like paths, plus every existing test case. They returned the same result on every input.
  • New positive cases: sudo rm -rf /, /bin/rm -rf /, rm -v -rf /etc, rm -rf build /etc, rm -rf a/rm /, rm -rf.x -rf /, cd /app && rm -rf ~.
  • New negative cases: rm -rf.x /, rm / -rf.
  • New test_destructive_shell_long_nonmatching_input_is_fast: six long nonmatching inputs (repeated flags, repeated rm, repeated rm -rf, rm-suffixed paths, a single huge flag token). Each must finish in under 0.5 s and score False through the scorer.
  • pytest tests/unit/score/regex/: 1225 passed. ruff check and ruff format --check are clean.

No documentation changes needed.

The "Destructive Shell" pattern let any repeated flag serve as the
recursive/force flag, so long nonmatching input such as
`rm -rf -rf ... relative` backtracked quadratically (and cubically when
`rm -rf` itself repeats), blocking the event loop during async scoring.

Anchor the rm branch at the start of a command segment and commit, via
atomic groups, to the first `rm` and first flag before searching for a
protected target. Detection is unchanged; add long nonmatching-input
regression tests.

Fixes microsoft#3698

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: ShellCommandOutputScorer has nonlinear matching cost for repeated nonmatching arguments

1 participant