Skip to content

fix(gates): irrecoverable reads a substitution glued to a git global as its value; chmod reads its mode word only (GH #405) - #407

Merged
wasikarn merged 2 commits into
developfrom
claude/gate-405-braced-glue
Oct 4, 2026
Merged

wasikarn merged 2 commits into
developfrom
claude/gate-405-braced-glue

Conversation

@wasikarn

@wasikarn wasikarn commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Closes #405.

Root cause (F1)

The issue suspected _split_ops and brace handling. That is not the cause. _blank_substitutions turns ${R}, $(...) and backticks into one placeholder (PH) inside the word. So -C${R} reaches shlex as one plain token, -C\x01. It is never split, so it is never _Glued, and _git_words (GH #375) returns nothing. The git walk strips PH before it checks for a bare value global. So -C\x01 reads as a bare -C: it takes reset as its value, and --hard becomes the subcommand. The unbraced -C$R is not blanked. It splits into glued $/R tokens, so the #375 window already catches it.

Real git rejects a glued -C<path>. The shape still matters because an unquoted ${R} word-splits: with R=" /repo", git -C${R} reset --hard runs git -C /repo reset --hard.

Fix

  • _git_words(rest, ph_value) adds a second reading. In it, a value global followed by a PH (-C${R}, --git-dir$(x)) holds its own value, so the next word is the subcommand.
    • The ph_value=False call is unchanged. The True call appends a window only when such a flag exists.
    • A deny in either reading wins, so the empty reading still denies (git -C${D} /repo reset --hard, git -C${D} $R reset --hard).
    • A PH before or inside the flag name (${X}-C, -${X}C) is not a value. Those shapes behave as on develop.
    • In the new window, "$" stands in for the PH, the same way -C$R reads. Keeping the PH would match again and append the same window forever (one mutant hangs).
  • F2, _chmod_world: the regex now runs only on the mode word, which is the first word that is not an option. It uses .match on the words from there on, so it matches a subset of what the old .search matched. The recursive, / and ~ check is unchanged.

Note: develop already allows chmod 777 x and chmod a+rwx x (not recursive, not on / or ~). The GH #336 rule never covered them. They stay allowed and are now allow controls.

Differential (frozen develop copy vs this branch; compares exit code, stdout, stderr and journal calls)

corpus inputs diffs
every payload test-gates.sh sends to the gate (captured), plus 7 shapes with a substitution before or inside the flag 1,244 distinct 9 = the new asserts
replay of distinct Bash commands from local transcripts (seeded sample of 116,042) 60,000 0
seeded fuzz: git globals combined with ${...}, $(...), backticks and quotes (after, before and inside the flag), plus chmod modes, files and options 23,000 2,358

Diff classes (fuzz + fixtures):

  • braced variable glued after a git global, allow -> deny: 636 + 5
  • $(...), $((...)) or backtick glued after a git global, allow -> deny: 275 + 1. Same blanking path, same root cause.
  • chmod whose non-mode word looks like 777 or a+rwx, deny -> allow: 1,447 + 3. None of these has a dangerous mode word.
  • other: 0. No deny -> allow outside chmod. No changed message on a command denied by both. No exit code other than 0 or 2.

Develop already denies all 911 new git denies from the fuzz when they are spelled -C$R (the GH #375 reading). Fuzz verdicts are identical under Python 3.9.6. Under load, a few git add cases flaked on both sides in the gate's own git mid-merge probe. They were identical on reruns.

Mutation

The new asserts killed all 10 mutants.

Timing

Best of 3 runs, as a subprocess. The largest #375 timing fixture (git + 4000 -C $R + reset --hard) took 0.11 s before and 0.08 s after (noise). 4000 -C${R} took 0.10 s on both. 2000 git -C${R} status; statements followed by a reset took 0.14 s before and 0.15 s after; that command is now denied and is added as a _timed_case.

@wasikarn
wasikarn merged commit 74c8254 into develop Oct 4, 2026
7 checks passed
@wasikarn
wasikarn deleted the claude/gate-405-braced-glue branch October 4, 2026 03:46
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.

gate: irrecoverable allows git -C${R} reset --hard (braced var glued to a git global); chmod rule over-blocks a 777 path

1 participant