Deny filesystem access by default in the process backend - #287
Merged
Conversation
A process sandbox whose policy named no filesystem paths received no
filesystem confinement at all. confine._apply_landlock returned early
before building a ruleset, so the guest kept full read access to the host
and could write anywhere:
sb = iso.spawn("p", backend="process", allowed_imports=["os"])
sb.exec("post(open('/etc/passwd').read())") # -> contents
sb.exec("open('/tmp/owned','w').write('x')") # -> succeeds
while the same report said seccomp was active, so the sandbox looked
confined. This contradicted the deny-by-default posture the import
allow-list already takes (DEFAULT_ALLOWED_IMPORTS is deliberately empty
"so missing import policy fails closed").
The early return also meant require_landlock was never consulted on that
path, so hardened rollout mode silently produced an unconfined guest on a
kernel without Landlock -- the one mode whose whole purpose is to refuse.
Always handle the filesystem access class. Landlock is default-deny for
every right its ruleset handles, and _runtime_read_paths() already grants
the interpreter what it needs to keep running, so the fix is to stop
skipping the ruleset rather than to build new machinery: the guest keeps
its stdlib and shared libraries, and home directories, /root, /var and all
writes are denied by the kernel.
The network layer is deliberately left as it was. Landlock keys network
rules on port and is default-deny for ports it does not name, so handling
that class with no allow-list would sever egress the policy never meant to
restrict.
/etc stays readable: the loader cache, TLS trust store and locale data live
there. Narrowing it to specific entries is worth doing separately.
Reported as landlock_default_deny_fs in the confinement report, so
"confined to policy" and "confined to the bare minimum" are distinguishable
by callers and by CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ebvMQ3vLxdK3joymz6Feg
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.
The problem
A process sandbox whose policy names no filesystem paths got no filesystem confinement at all.
confine._apply_landlockreturned before building a ruleset. Measured onmain:…while the same sandbox's confinement report said
seccomp: True, so it looked confined. This contradicts the deny-by-default posture the import allow-list already takes —DEFAULT_ALLOWED_IMPORTSis deliberately empty "so missing import policy fails closed."Second, less visible bug: that early return happened before
require_landlockwas consulted. So hardened rollout mode — the mode whose entire purpose is to refuse rather than degrade — silently produced an unconfined guest on a kernel without Landlock.The change
Always handle the filesystem access class. Landlock is default-deny for every right its ruleset handles, and
_runtime_read_paths()already grants the interpreter its stdlib and shared libraries. So the fix is to stop skipping the ruleset, not to build new machinery:/root,/var,/opt,/srv, and all writes → denied by the kernelskipped, and fatal under hardened modedefault_deny_fs=Falseopts out.The network layer is deliberately unchanged. Landlock keys network rules on port and is default-deny for ports it does not name, so handling that class with no allow-list would sever egress the policy never meant to restrict.
/etcstays readable — the loader cache, TLS trust store, and locale data live there. That is a real residual:/etc/passwdremains readable (though it is world-readable on every Unix, and/etc/shadowis root-only while the guest is not root). Narrowing/etcto specific entries is worth doing, but it is a riskier change and belongs in its own PR.New
landlock_default_deny_fsfield in the confinement report distinguishes "confined to policy" from "confined to the bare minimum".This container's kernel reports Landlock ABI 0, so I could not verify live enforcement. I did not want to claim a result I hadn't seen, so I proved the mechanism two ways instead:
handled_access_fswhen no policy paths are given, that exactly the interpreter's runtime paths are added as rules, and thatlandlock_restrict_selfis called. This is the actual mechanism that makes everything else denied.@live_landlock) assert a policy-free guest cannot read or write outside the interpreter tree and can still import the stdlib. These skip here but run in thetests-cross-kernelCI job, which already setsPYISOLATE_LIVE_LANDLOCK_TESTS=1on ubuntu-22.04/24.04.The CI run on this PR is the real check on enforcement. The
can_still_import_the_stdlibtest is the one to watch — it is the canary for default-deny breaking a guest.Tests
12 new. Seven run on any kernel, including the hardened-mode regression (
require_landlock=Truewith no policy paths now raises instead of returning silently) and the report-records-the-gap case. Five require a Landlock kernel.Full suite 512 passed / 11 skipped (was 505/6 — the 5 new skips are the live tests). flake8 clean.
Behavior change to be aware of
Hardened rollout mode with
backend="process"on a kernel without Landlock now raises where it previously succeeded unconfined. That is the intended fail-closed semantics, but it will surface as a new failure for anyone running hardened mode on such a kernel.SECURITY.md §2.3,
docs/threat-model.md§boundary and its History section are updated.Generated by Claude Code