feat: auto-degrade when jail setup is denied inside the child #287
No reviewers
Labels
No labels
burndown-2026-06
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra!287
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/auto-degrade-mode2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #277.
docs/sandbox.mdnamed two denial modes and umbra handled one.cmd.RunreturnsEPERM/EACCESwith a nilProcessState,SetupDeniedreports it,shell.Runnerretries unsandboxed. Already handled.ProcessStateis non-nil andSetupDeniedreturns false atcli/shell/shell.go:105. The verb died.Mode 2 is the live one - Ubuntu 24.04's
apparmor_restrict_unprivileged_userns=1hits it. With no escape, the only remedy wasCLIGUARD_NO_SANDBOX, which is exactly why it spread to 30 standing declarations across the fleet instead of staying a deliberate choice.The mechanism
Wrapattaches a sentinel pipe throughExtraFiles. The jail child writes one byte when setup is refused, andshell.Runnertakes the existing retry path.A dedicated fd was chosen over a reserved exit code because a wrapped tool can exit with any code, and a false positive here silently drops a security boundary. Three properties hold:
jailSetupperforms every step and returns the target;execJailTargetis its only caller. An error means the tool never ran, so the retry cannot double an effect.EPERM/EACCESdegrade. A malformed spec, missing path, or read-only filesystem still fails rather than quietly dropping the jail.Verified on real Linux, with a forced mode-2 denial
Docker, root-owned
/tmpand uid 1000, so the child's stashmkdiris refused after the namespace is created - genuine mode 2, not the already-handled mode 1:No
CLIGUARD_NO_SANDBOXset in either run. That is #277's acceptance criterion.I checked that this actually exercises mode 2 rather than flattering itself: in a default Docker container both the old and new binaries succeed, because that environment denies the clone and hits mode 1. The comparison above only diverges once the denial moves inside the child.
Also: full suite green on Linux in Docker, and the darwin gate (
build,vet,tidy,test,lint0 issues).Test coverage, and its limit
Six new Linux unit tests cover the channel: fires on the sentinel, ignores foreign bytes, does not block on a silent child (a bug I hit and fixed - the parent must drop its own write end or
Readhangs forever), nil-safe, and theEPERM/EACCES-only classification.What is not automated: the end-to-end environmental proof above is a documented Docker invocation, not a CI test. Forcing a real mode-2 denial needs a namespace-capable runner and a hostile
/tmp, which the unit tests cannot fake without adding a fault-injection hook into the security path - which I judged worse than the gap. The mechanism is unit-tested; the environment behaviour is reproducible by hand.The env var stays
CLIGUARD_NO_SANDBOXremains a deterministic opt-out that skips the per-exec retry, not a correctness requirement.ward containerstill sets it deliberately, since the container is already the boundary.docs/sandbox.mdnow says that instead of telling operators to set it.Stripping the fleet's 30 standing declarations follows separately, once this releases.
Pull request closed