Enforce filesystem policy on guest processes with Landlock#269
Merged
Conversation
Translates the sandbox's filesystem policy into a kernel Landlock ruleset applied to the guest process, so a guest that defeats the Python open guard and reaches the real open/openat can still only touch permitted paths. Applied after PR_SET_NO_NEW_PRIVS (which landlock_restrict_self requires) and before the seccomp lockdown. - runtime/landlock.py: ABI-versioned ruleset construction. Queries the kernel Landlock ABI and masks the handled access-rights set to it (requesting an unsupported right fails ruleset creation); grants read+execute on the interpreter's runtime paths (stdlib, shared libs) so the guest can keep importing, read on policy read paths, and read+write on policy write paths; everything else the ruleset handles is denied. Packed path_beneath struct, O_PATH parent fds. - confine.apply_confinement now sets no_new_privs first, then rlimits, then Landlock (only when the policy names paths -- a default-deny ruleset with no allow-list would just break the interpreter), then seccomp. - process_backend extracts read vs write paths from the policy and passes them in the bootstrap; the confinement report gains landlock fields. - Fixes a latent bug in _extract_fs_tcp: the legacy Policy exposes allow_fs/allow_tcp as *methods*, so it must be discriminated from RuntimePolicy (which exposes rule tuples) by type, not getattr. Landlock is unavailable on many kernels; it is applied best-effort and recorded in the report (hardened mode can require it). Enforcement tests are gated on landlock_supported() plus PYISOLATE_LIVE_LANDLOCK_TESTS, like the live BPF test; the ABI-masking, struct-packing, and policy-extraction logic is unit-tested unconditionally. CI runs the live Landlock test on the cross-kernel matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR
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.
Why
#268 stops the guest from making dangerous syscalls, but the sandbox's filesystem policy was still enforced only by the Python
open/import guards — which a guest that reaches the realopen/openat(native code, or the object-graph escape) bypasses. This PR turns the policy's filesystem allow-list into a real kernel boundary using Landlock. (Builds on #267 and #268, both merged.)What
New
runtime/landlock.pybuilds and applies a Landlock ruleset to the guest process, wired intoconfine.apply_confinement():landlock_create_ruleset(NULL, 0, VERSION)) and masks the handled access rights to it — requesting an unsupported right (e.g.TRUNCATEon ABI < 3) makes ruleset creation fail.landlock_supported()is authoritative.sys.path) in addition to the policy's paths; otherwise it couldn'timportanything.no_new_privsfirst (required bylandlock_restrict_self), then rlimits, then Landlock, then seccomp. Landlock is applied only when the policy actually names paths — a default-deny ruleset with no allow-list would just break the interpreter.ProcessSandboxextracts read-vs-write paths from the policy and passes them in the bootstrap; the confinement report gainslandlock/landlock_rules.A latent bug this fixes
_extract_fs_tcp(from #267) treatedgetattr(policy, "allow_fs")as a rule collection — but the legacyPolicyexposesallow_fs/allow_tcpas methods, so passing a legacyPolicyto a process sandbox raisedTypeError: 'method' object is not iterable. Extraction now discriminatesRuntimePolicy(rule tuples) fromPolicy(methods +.fs/.tcpattributes) by type. Covered bytest_extract_fs_tcp_handles_legacy_policy_methods.Testing
tests/test_landlock.py. Unit tests (always run): ABI access-mask correctness, packedpath_beneathstruct size, support/ABI consistency, and policy read/write extraction incl. the legacy-Policy regression. Kernel-gated:test_process_sandbox_reports_landlock_applied(needs Landlock) andtest_landlock_blocks_disallowed_reads_but_allows_permitted(needs Landlock +PYISOLATE_LIVE_LANDLOCK_TESTS=1) — the enforcement test reads an allowed file via the realopenand asserts a read outside the allow-list is denied.Honesty note
Landlock enforcement is unavailable in the dev container this was written in (
landlock_create_ruleset→ENOSYS), so the live enforcement path is exercised by CI on capable kernels, not here — same pattern as the repo's existing live BPF test. Everything not requiring the kernel is unit-tested directly.Next
B-4 wires the eBPF policy maps end-to-end (fixing the map-name/schema disconnect), then B-5 reconciles the threat model now that the boundary is real.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR
Generated by Claude Code