You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
plugins/guardrails/hooks/block-root-delete-target.sh unwraps launchers from an allow-list; an unlisted launcher ends the walk and its own name becomes the command word (declared gap, :172-176). After #4469 and #4520, these still exit 0 on current main, each a recursive delete of /:
command
rc
sg root -c 'rm -rf /'
0
setpriv --reuid=0 rm -rf /
0
prlimit --nofile=10 rm -rf /
0
systemd-run rm -rf /
0
env --spl='rm -rf /' (abbreviated --split-string)
0
nsenter --t 1 rm -rf /* (abbreviated --target)
0
POSIXLY_CORRECT=1 su -s env x su -s env x rm -rf /
0
sudo -R /mnt rm -rf /
0
sudo --chroot /mnt rm -rf /
0
sudo -Eu bob rm -rf /
0
sudo --us bob rm -rf /
0
Controls on the same run: rm -rf /, env --split-string='rm -rf /' and nsenter --target 1 rm -rf /* exit 2.
The four sudo rows are declared gaps (:177-183): the guard reads -R/--chroot, a short cluster ending in an operand-taking letter, and an abbreviated long option as flags without an operand, so the next word becomes the command word. Reading them correctly changes how sudo lines the guard refuses today are read (sudo -R rm -rf / would take rm as the chroot directory), which is why they need a differential.
Evidence
Measured this pass (2026-09-27, main a6ba321, Windows Git Bash), JSON payload on stdin to the guard directly, nothing executed. Every row above was run twice: without a cwd field and with cwd set to a git checkout (so #4520's outside-tree arm was active); results were identical, including sg root -c 'rm -rf /etc' and setpriv --reuid=0 rm -rf /etc at 0 while rm -rf /etc is 2.
Not reproduced with the shapes tried (the item did not record exact payloads): env -S losing backslash provenance (env -S 'rm -rf' \/, env -S 'rm -rf \' /, env -S "rm -rf \\/" all exit 2), and eval repeated about 3,000 times timing out (eval x3000 + rm -rf / refused in 4 s by the eval-length budget; eval :; x1800 + rm -rf / refused in 5 s by the 1,024 nested-reading budget).
Proposed approach
Add sg (its -c operand runs through a shell, so re-parse it like su -c), setpriv, prlimit and systemd-run to the launcher table with their operand-taking options, so the word after the options is the command word.
Find why the POSIXLY_CORRECT su chain passes (POSIXLY_CORRECT stops getopt at the first non-option, :620-622) and read it the way su does.
Sudo spellings: do not switch to the operand-taking reading, because that alone moves sudo -R rm -rf / from 2 to 0 (rm becomes the chroot directory). Judge both readings and refuse if either is a recursive root delete, the shape rdt_resolved_walk (:879-890) already uses for abbreviated launcher options. The same two-reading walk covers env --spl and nsenter --t.
Problem
plugins/guardrails/hooks/block-root-delete-target.shunwraps launchers from an allow-list; an unlisted launcher ends the walk and its own name becomes the command word (declared gap,:172-176). After #4469 and #4520, these still exit 0 on current main, each a recursive delete of/:sg root -c 'rm -rf /'setpriv --reuid=0 rm -rf /prlimit --nofile=10 rm -rf /systemd-run rm -rf /env --spl='rm -rf /'(abbreviated--split-string)nsenter --t 1 rm -rf /*(abbreviated--target)POSIXLY_CORRECT=1 su -s env x su -s env x rm -rf /sudo -R /mnt rm -rf /sudo --chroot /mnt rm -rf /sudo -Eu bob rm -rf /sudo --us bob rm -rf /Controls on the same run:
rm -rf /,env --split-string='rm -rf /'andnsenter --target 1 rm -rf /*exit 2.The four sudo rows are declared gaps (
:177-183): the guard reads-R/--chroot, a short cluster ending in an operand-taking letter, and an abbreviated long option as flags without an operand, so the next word becomes the command word. Reading them correctly changes how sudo lines the guard refuses today are read (sudo -R rm -rf /would takermas the chroot directory), which is why they need a differential.Evidence
Measured this pass (2026-09-27, main a6ba321, Windows Git Bash), JSON payload on stdin to the guard directly, nothing executed. Every row above was run twice: without a
cwdfield and withcwdset to a git checkout (so #4520's outside-tree arm was active); results were identical, includingsg root -c 'rm -rf /etc'andsetpriv --reuid=0 rm -rf /etcat 0 whilerm -rf /etcis 2.Not reproduced with the shapes tried (the item did not record exact payloads):
env -Slosing backslash provenance (env -S 'rm -rf' \/,env -S 'rm -rf \' /,env -S "rm -rf \\/"all exit 2), andevalrepeated about 3,000 times timing out (evalx3000 +rm -rf /refused in 4 s by the eval-length budget;eval :;x1800 +rm -rf /refused in 5 s by the 1,024 nested-reading budget).Proposed approach
sg(its-coperand runs through a shell, so re-parse it likesu -c),setpriv,prlimitandsystemd-runto the launcher table with their operand-taking options, so the word after the options is the command word.env(--split-string) andnsenter(--targetand the rest of its operand-taking long options).POSIXLY_CORRECTsu chain passes (POSIXLY_CORRECT stops getopt at the first non-option,:620-622) and read it the way su does.sudo -R rm -rf /from 2 to 0 (rmbecomes the chroot directory). Judge both readings and refuse if either is a recursive root delete, the shaperdt_resolved_walk(:879-890) already uses for abbreviated launcher options. The same two-reading walk coversenv --splandnsenter --t.Files:
hooks/block-root-delete-target.sh,hooks/block-root-delete-target.test.sh, README row, CHANGELOG, plugin.json.Acceptance criteria
cwd.sg root -c 'ls /',setpriv --reuid=0 ls,prlimit --nofile=10 lsandsystemd-run lsexit 0.sudo -R rm -rf /and every other sudo line main refuses still exit 2.Constraints and gotchas
:184-192). Guards only add refusals.timeoutduration under a comma locale.Context
Source: local handoff item 20260925-070000-guardrails-root-delete-launcher-remainder-after-4469.md, items 1, 3, 4 and 5 (retired into this issue; item 2 is draft 20260925-070000-b). Prior: #4468 / PR #4469, #4519 / PR #4520. Related: #4516 (PowerShell
Remove-Item -Recurse), #4242 (wslprefix).