Skip to content

fix(guardrails): unwrap sg, setpriv, prlimit and systemd-run in root-delete #4685

Description

@kyle-sexton

Problem

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

  1. 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.
  2. Extend the resolved abbreviated-long-option walk fix(guardrails): unwrap runuser, taskset and the launcher family in the root-delete guard #4469 added to env (--split-string) and nsenter (--target and the rest of its operand-taking long options).
  3. 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.
  4. 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.

Files: hooks/block-root-delete-target.sh, hooks/block-root-delete-target.test.sh, README row, CHANGELOG, plugin.json.

Acceptance criteria

  • Every rc-0 row above exits 2, with and without a payload cwd.
  • sg root -c 'ls /', setpriv --reuid=0 ls, prlimit --nofile=10 ls and systemd-run ls exit 0.
  • sudo -R rm -rf / and every other sudo line main refuses still exit 2.
  • The differential against main shows zero cells where main refuses and the branch allows, over the existing refusal corpus plus these rows (the fix(guardrails): unwrap runuser, taskset and the launcher family in the root-delete guard #4469 gate ran more than 200 payloads with 0 looser than main).
  • Every probe in the tests is a JSON payload on stdin.

Constraints and gotchas

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 (wsl prefix).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.priority: mediumReal value, no hard deadline; normal backlog flow.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions