Repository navigation
[security] Kernel enforcement can be absent without anyone noticing #123
Copy link
Copy link
Closed
Labels
securitySecurity hardening, sandbox or SSRF residualSecurity hardening, sandbox or SSRF residual
Description
Context
The containment layer is platform- and kernel-dependent. openat2 needs Linux >= 5.6; the no-follow walk needs openat, which other platforms lack. Where that is missing, kernelEscapeCheck returns false and openBeneathRoot reports "unsupported", so the caller falls back to a plain open after the portable userspace checks.
The fallback is correct and deliberate, but it is a capability downgrade that nothing reports. It is visible only in code comments and debug logs. Neither the tool result nor the session record says "this ran with userspace checks only", so:
- a caller that needs a hard boundary cannot tell the difference;
- a silent downgrade cannot be caught in CI, on a platform, or after a kernel change;
- the same will be true of the command-confinement backend once it exists (there it is worse: no backend means no boundary at all).
What to build
- An enforcement fact per operation — at minimum
kernelvsuserspace, extended tofull/partialonce a command backend exists — reported in the tool result next to the existing sandbox facts and persisted with the session. - A startup probe that records which containment mechanisms and which command-confinement backend are actually usable, and surfaces the verdict once (log + runtime context) instead of failing at first use.
- An opt-in "require a hard boundary" setting: when on, an operation that would fall back to userspace-only checks is refused with a clear error rather than silently weakened.
Acceptance
- A test that disables the kernel path asserts the result carries the
userspacefact; with the hard-boundary requirement on, the same operation is refused. - On a platform without
openat2, the fact is visible in a tool result — not only in a log line. - The startup verdict is deterministic and testable on a machine where the backend is missing, and a test asserts the probe runs once per process.
- The fact appears in the persisted session record, so a later reader can tell how an operation was enforced.
Related: #121 (kernel boundary for bash).
Verification
- Go. The enforcement fact is
kernelwhere the kernel path is active anduserspacewhere it is disabled; with "require a hard boundary" on, the same operation is refused. The startup probe runs once per process, and its verdict is deterministic on a host without the backend. - Persistence. The fact is asserted in the written session record, so a later reader can tell how an operation was enforced.
- Scenario — only if the fact gets a screen representation. A fact that lives in a tool result is invisible: the TUI renders tool calls, not result text, and nothing else shows it. Recommended companion: a
/sandboxcommand (or a status-bar indicator) printing the active backend, the enforcement level, and the effective writable roots. Thentui/testdata/scenarios/confine-status.scenarioasserts that output, including its degraded wording on a host without the kernel path — and a silent downgrade becomes visible to the person running the agent, not only to a debug log. Without that representation this issue stays Go-only.
Landing plan and progress: #127.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
securitySecurity hardening, sandbox or SSRF residualSecurity hardening, sandbox or SSRF residual
Activity
feat(tool): report what confines file operations, and refuse to weake…
yusiwen commentedon Oct 8, 2026
Landed in PR #129, rebase-merged onto
masteras67397cb.What landed
ContainmentInfo()probes once per process for the mechanism the open layer will actually use, and names it:openat2 RESOLVE_BENEATH, the component walk, or in-process checks.kernel, so a degraded host is visible in the result the model reads and ordinary results are unchanged./sandboxprints level, mechanism, whether a hard boundary is required, and the effective writable roots.sandbox.require_hard_boundaryrefuses an operation that would be enforced by in-process checks alone, including when no project root is configured.ELOOP, which is a kernel decision attached to the open.Measurements
go test ./... -count=1→ 13 packages ok / 0 failed;-race→ 13 packages ok / 0 failed;make staticcheckexit 0;gofmt -lempty;go vet ./...clean.apply_patchmutation under a disabled kernel path carries the note; the hard-boundary policy proceeds on a kernel host, refuses withErrHardBoundaryUnavailableon a userspace host and with no project root, and leaves the weaker boundary usable when off; the fact survivesFlush/Loadand appears in the raw JSON.tui/testdata/scenarios/sandbox-command.scenario→ ok 13 steps; the rendered screen readscontainment: kernel (openat walk, O_NOFOLLOW per component)on macOS and lists one writable root.ad5a059; annotations at baseline, no new ones.Acceptance sentence reinterpreted, not fudged. The issue said "on a platform without
openat2, the fact is visible in a tool result". macOS has noopenat2but does enforce the boundary through the component walk, so pinning "no openat2 ⇒ userspace" would have misreported a real boundary as missing. The level is thereforekernelfor both kernel mechanisms, anduserspacemeans no kernel mechanism at all (the!linux && !darwinbuild). Recorded in the PR under "Honest scope".Handed off. #130: the first CI run of this PR failed in
permission-allow-always.scenarioatwait-exit, because two Ctrl+C presses that both land while the run is streaming cancel the run and arm no quit. Diagnosed, filed with a capture recipe, and fixed separately rather than folded in here.