Repository navigation
fix(gates): irrecoverable reads $IFS splits, variable-held verbs, mkfs, chmod 777 (GH #336) - #376
Merged
Merged
Conversation
…s, chmod 777 (GH #336) New pattern rules: mkfs (mkfs, mkfs.*, mke2fs with an operand), chmod-world (777 / a+rwx with -R or on / or ~) and rm-no-preserve-root (any unambiguous prefix). Two views, each a second reading of the whole command checked by every rule and owned by a rule id of its own: ifs-split ($IFS, ${IFS}, ${IFS:..} read as a blank, empty positionals dropped) and var-verb ($NAME read back where the command assigns NAME a plain value). Views run after the command passed, up to 20 KB; a structural deny inside a view only ends that view. An ask floor for a `$NAME` program with no visible assignment (opaque-var-verb) and for `source`/`.` (source-file). The fast path now sends mkfs, mke2fs, chmod and source to python. Shadow first (GH #337): var-verb, opaque-var-verb and source-file. A 55,000-command replay of real transcripts with every new rule shadowed gave 0 verdict diffs against develop and these would-be hits: var-verb 1 (a real `git -C $R checkout -- <file>`), opaque-var-verb 63 asks, source-file 98 asks; mkfs, chmod-world, rm-no-preserve-root and ifs-split 0, so those enforce.
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 #336. Builds on #369 (GH #337 shadow mode).
What
hooks/gates/irrecoverable.pygains three pattern rules, two "views" and an ask floor:mkfsmkfs,mkfs.*,mke2fswith an operand (mkfs.ext4 -Vstays allowed)chmod-world777/0777/1777/a+rwx/ugo=rwx, with-R/--recursiveor a target of/,~(and/*,~/)rm-no-preserve-rootrmwith--no-preserve-rootor any unambiguous prefix (--n...)ifs-split(view)$IFS,${IFS}and${IFS:..}as a blank, and$9/$@-style empty positionals droppedvar-verb(view)$NAME/${NAME}replaced by a plain value the command assigns to NAME (last value, then first)opaque-var-verb$NAMEwith noNAME=anywhere in the commandsource-filesource <file>/. <file>A view is a second reading of the whole command, checked by every rule. It runs only after the command passed, and only up to 20 KB. Inside a view a verdict belongs to both the matched rule and the view: if either is shadowed, the verdict is shadowed. A structural deny inside a view (an unbalanced quote the substitution created) only ends that view.
irrecoverable.sh's fast path now sendsmkfs,mke2fs,chmodandsourceto python.The issue says the four rows should deny. Three of them do.
X=rm; $X -rf buildships as a shadowwould_deny, because of the task rule that a rule which denies real transcript commands ships shadow (see the replay below). Promote it by deleting"var-verb"fromSHADOW_RULES.Replay: real false-deny count (55,000 distinct Bash commands from local transcripts, 29,439 of them subagent)
Every new rule shadowed, against a frozen copy of develop: 0 verdict, stdout or stderr diffs, no exit other than 0/2, worst time 0.18 s. Would-be hits per rule:
R=...; git -C $R checkout -- <file>. This is a true discard that develop misses:-Ctakes the$token as its value. It still denies a real command, so the rule ships shadow. The general bypass (git -C $VAR <sub>, no assignment needed) is filed as #375.The first replay found 61 var-verb hits. Two fixes brought it to 1. First, values are now read from the raw text (
_PLAIN_ASSIGN_RE), not from the tokens, which splitRW=$R/xinto an empty assignment. Second, a view no longer re-counts an ask that the command already asked, or a structural deny.Tests (TDD: the new rows were red on develop before the fix)
tests/hooks/fixtures/irrecoverable-336-cases.txt, replayed bytest-gates.sh: 72 cases (DENY / ALLOW / WOULD_DENY / WOULD_ASK). Each WOULD_* line is also run against a copy withSHADOW_RULESemptied, and it must deny or ask there. 28 benign controls:IFS= read,OLDIFS=$IFS,"$IFS",X=1; echo $X,PY=python3; $PY -m pytest,chmod +x,chmod -R 755,chmod 777 /tmp/x,chmod 644 ~/.ssh/config, and the replay's own former false deny (RW=$R/w; git -C $RW remote add ...). An ALLOW control fails if it journals anywould_*row.test-gate-journal.shcase 7: withifs-split/var-verbshadowed, the view's deny is journaled under the view's id and allowed, and a plainrm -rfstill denies. Enforced, it denies with a row that names the view.test-gates.sh: 1,247/1,247.test-gate-journal.sh: 28/28. The gauntlet passes (GAUNTLET_JOBS=2).harness-auditreports 0 CRIT.${IFS:..}, positionals kept, chmod without -R / without a+rwx / without//~, mkfs dotted-only, mkfs without the operand check, no-preserve-root off, opaque ignoring assignments, source off, view shadow by matched rule only, structural deny firing in a view, assignments read from tokens, fast-path words dropped, var-verb enforced.Real shells (argv-logging stubs; bash 3.2, bash 5.3, dash, zsh, /bin/sh)
Every DENY and WOULD_DENY shape in the fixture ran the destructive argv in at least bash and sh. zsh does not split an unquoted
$IFS(it runsrm${IFS}-rfas one word), and dash has no${IFS:0:1}. Every other shape ran in all five shells.Fuzz (own generator, 4,500 cases x 2 seeds, plus 1,148 recorded test-gates payloads)
->allow). Tightened: seed 1 has 773 allow->deny, 12 ask->deny and 6 allow->ask; seed 2 has 759, 7 and 5. The rest of the diffs are deny->deny with a different message. Re-run in real shells with stubs, 1,341 of the 1,562 tightened cases really ran a destructive argv. The others are the safe direction: they depend on control flow (a || chmod -R 777 x), usebuiltin <non-builtin>(develop already treatsbuiltinas a wrapper), or zsh does not split$IFS.Timing (worst cases, M3 Pro, load ~5)
eval sudopadding + a laterm${IFS}-rfchmod -R a+rw ...words$IFS, over the view capThe new regexes are linear. On 20 KB adversarial inputs (unbalanced
X='runs, a 20 KB value ending in$),_PLAIN_ASSIGN_REtakes 1 ms.Residue
. filewith no candidate word never reaches python, sosource-filecannot see it.${X}as argv0 is not read asmkfs(it would turn every${EDITOR} finto a mkfs deny).mk$(true)fsis read as mkfs.$IFSand a variable read back is not checked in one view.No manifest bump.