Repository navigation
fix(gates): subagent guard reads a zsh glued brace group as a command start (GH #318) - #321
Merged
Merged
Conversation
#318) zsh runs a brace group with no blank after the open brace, so `{git stash;}` ran git stash under the Bash tool's zsh while the guard's anchor only took `{` followed by a blank. A `{` that starts a word (line start, a blank, or one of `;&|(){}` before it) now opens a command position too; `{` + blank keeps develop's reading everywhere. `x{git stash;}` and `${git stash;}`, which no shell runs, stay allowed. Closes #318
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #318.
What
zsh runs a brace group with no blank after the open brace (
{git stash;}); bash, dash and ksh read{gitas a word. The Bash tool runs zsh, so this was a live subagent-guard bypass for plain stash, reset and clean. The guard's command-start anchor took{only when a blank followed._cmd_startnow also takes a glued{that starts a word: line start, a blank, or one of;&|(){before it. The old{+ blank alternative is kept as is, so every command develop denies is still denied. The lookbehind set was read off real zsh: it runs(){git stash;},f(){git stash;},{{git stash;};},2>&1 {git stash;},for i in 1; {git stash;}, and does not runx{git stash;},${git stash;},\{git stash;}or{true;}{git stash;}.Tests
tests/hooks/test-subagent-git-guard.shsection (23): 25 deny rows (each checked to run git stash/reset/clean in zsh with a logging git stub), 9 allow controls (each runs nothing in sh, bash 3.2, bash 5, dash, zsh, ksh), and 2 timing rows (3000 glued braces, deny and allow shapes both decided inside 8 s). All 25 deny rows were red before the fix.Mutation check of the new rows: 13 gate mutants, 13 killed (revert to develop, drop the brace start, no lookbehind, lookbehind without
), without{, with}, without blanks, plus 6 on neighbouring logic). Every one of the 34 new rows flips under at least one mutant.Differential fuzz (real shells as oracle, any deny wins)
Own generator, 4000 cases, oracle = sh, bash 3.2, bash 5, dash, zsh, ksh with logging stubs:
/usr/bin/env git stash, runs in every shell), filed as gate: subagent guard does not see a wrapper written as a path (/usr/bin/env git stash) #320; 3 are ksh93's${ cmd;}substitution with a glued redirect (${</dev/null git stash }), ksh only.! {{eval git stash pop;}, a zsh parse error). Safe direction.gitand{: 0 newly denied, 0 newly allowed, worst case 0.28 s.Validation
test-subagent-git-guard.sh558/558,test-gates.sh1014/1014, harness-audit 0 CRIT,run-gauntlet.shall layers passed.