Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/reference/repo-gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ builder's self-check ("0 bypasses in 4032") was wrong each time.
bound to a shared budget and denies past it (GH #246: 30 KB of `env ; ` before a `git stash`
timed out into allow). Only 3 of 2,581 replayed real commands (20-28 KB scripts) hit it.
GH #248 added a third, lazy-walk pass (`*?` finds the FIRST target after a wrapper, where the
greedy walk lands on the last), `{` + blank as a command start, and `xargs` in the spawn
greedy walk lands on the last), `{` + blank as a command start (GH #318: also a glued `{` that
starts a word, since zsh runs `{git stash;}`), and `xargs` in the spawn
anchor's wrapper list only. Each pass charges the budget, so the guard's rose to 60M; a padded
benign command of ~3,000 blank lines (spawn scan) or ~2,100-2,200 `{ ` units (both gates) is now refused.
GH #276: the lazy pass can land on a wrapper argument spelled `git` (`sudo -u git env git stash`),
Expand Down
6 changes: 5 additions & 1 deletion hooks/gates/subagent-git-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,12 @@ def _mask_quotes(s):
# GH #285: a redirection (operator + its word) may sit before the command word and hid it
# (`</dev/null git stash`, `<<EOF git stash`); skipped like a VAR=val, in any mix with them.
_REDIR = r"(?:(?:\d+|\{\w+\})?(?:<<<?-?|&>>?|[<>]&|<>|>\||[<>]>?)[ \t]*[^\s;&|()<>]+[ \t]+)"
# GH #318: zsh also runs a brace group with no blank after `{` (`{git stash;}`); bash, dash and ksh
# read `{git` as a word. The Bash tool runs zsh, so a glued `{` opens a command position too (any
# deny wins) when it starts a word: zsh runs `(){git stash;}` and `{{git stash;};}`, but not
# `x{git stash;}`, `${git stash;}` or `{true;}{git stash;}`. `{` + blank keeps develop's reading.
def _cmd_start(wrapper_prefix):
return (r"(?:^|[|;&(]|&&|\|\||\{(?=\s))\s*" + _KEYWORD_PREFIX +
return (r"(?:^|[|;&(]|&&|\|\||\{(?=\s)|(?<![^\s;&|(){])\{)\s*" + _KEYWORD_PREFIX +
r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)|" + _REDIR + r")*" + _LEAD_CHAIN + wrapper_prefix + _CHAIN_PREFIX)
# GH #245: every anchor regex is scanned with overlapping matches, `(?=(...))`, read through
# m.end(1). The old wrappers' greedy argument walk (`time ls; git stash; git status`) crosses
Expand Down
53 changes: 53 additions & 0 deletions tests/hooks/test-subagent-git-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,59 @@ for _c in \
check "GH #307 allow shape finishes inside 8 s (rc $rc, ${#_c} bytes): ${_c:0:30}" "$ok"
done

# --- (23) GH #318: zsh runs a brace group with no blank after `{` (`{git stash;}`); bash, dash and
# ksh read `{git` as a word. The Bash tool runs zsh here, so a glued `{` is a command start too (any
# deny wins). Each deny row runs git stash/reset/clean in zsh (logging git stub); the controls run none.
for _c in \
'{git stash;}' \
'{git stash; }' \
'{git reset --hard;}' \
'{git clean -fd;}' \
'{git stash}' \
$'{git stash\n}' \
'true; {git stash;}' \
'true&&{git stash;}' \
'{ {git stash;}; }' \
'{{git stash;};}' \
'if true; then {git stash;}; fi' \
'! {git stash;}' \
'({git stash;})' \
'{env git stash;}' \
'{time git stash;}' \
'{A=1 git stash;}' \
'{git -C . stash;}' \
'{git stash;} 2>&1' \
'time {git stash;}' \
'{git stash;}; git status' \
'(){git stash;}' \
'f(){git stash;}; f' \
'for i in 1; {git stash;}' \
'2>&1 {git stash;}' \
'echo $({git stash;})' ; do
rc=$(sgg_rc "$_c"); ok=1; [ "$rc" = "2" ] && ok=0
check "GH #318 denied (zsh glued brace group): ${_c//$'\n'/<nl>}" "$ok"
done
for _c in \
'{git status;}' \
'{git stash list;}' \
'echo {git,x} stash' \
'{}git stash' \
'x{git stash;}' \
'${git stash;}' \
'{true;}{git stash;}' \
'echo {a,b} git stash' \
'git commit -m "fix {git stash;}"' ; do
rc=$(sgg_rc "$_c"); ok=1; [ "$rc" = "0" ] && ok=0
check "GH #318 control allowed: $_c" "$ok"
done
# Every glued `{` is now a command start; a long run of them must still be decided inside 8 s.
_c="$(_pad '{' 3000)git stash;}"
rc=$(sgg_rc8 "$_c"); ok=1; [ "$rc" = "2" ] && ok=0
check "GH #318 padded glued braces decided inside 8 s as deny (rc $rc, ${#_c} bytes)" "$ok"
_c="$(_pad '{' 3000)ls"
rc=$(sgg_rc8 "$_c"); ok=1; [ "$rc" != "124" ] && ok=0
check "GH #318 padded glued braces allow shape finishes inside 8 s (rc $rc, ${#_c} bytes)" "$ok"

echo ""
echo "=== $pass passed, $fail failed ==="
[ "$fail" -eq 0 ]
Loading