feat(py): the Landlock filesystem sandbox for the worker - #356
Draft
jat255 wants to merge 1 commit into
Draft
Conversation
engage() restricts the calling process to a set of read roots and write roots through the three Landlock syscalls, reached by number through ctypes because there is no libc wrapper for them. Read roots get execute, read-file and read-dir; the scratch directory gets everything the ruleset handles. The handled mask is computed from the ABI the kernel reports rather than fixed, since Landlock leaves any right a ruleset does not handle entirely unrestricted, and a fixed mask stops covering rights as kernels gain them. Each root is granted under both the path given and what it resolves to: a package library that is a farm of symlinks into a shared store, which is what Connect gives a deployed application, is otherwise granted a set of links pointing at content the worker still cannot read. A root that is not there is skipped, because failing to start over a directory this host happens not to have would be a sandbox that fails into not running. A kernel with no Landlock reports None rather than raising, which is what lets the user-namespace backend be chosen instead. A kernel that has it and then refuses a step raises, since that is a broken host and not an old one. sandbox_capabilities() now asks the kernel for its Landlock ABI instead of reporting the placeholder.
jat255
force-pushed
the
jat255/z5gn-landlock-sandbox
branch
from
September 11, 2026 19:48
d54a489 to
50c69de
Compare
jat255
changed the base branch from
jat255/t7d4-sandbox-gate-rlimits
to
jat255/s87h-seccomp-network-block
September 11, 2026 19:48
jat255
added this pull request to stack #364
September 11, 2026 19:48
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.
This PR adds
_landlock.engage(), which restricts the worker to a set of read roots and a writable scratch directory using Landlock, reached throughctypesbecause the three syscalls have no libc wrapper.sandbox_capabilities()now asks the kernel for its Landlock ABI instead of reporting the placeholder.Stacked on #362; review the PRs below it first. Tracks kata
z5gn.Agent-written detail
The handled-access mask is built from the ABI the kernel reports, because Landlock leaves any right a ruleset does not handle unrestricted. ABI 5 is the newest that adds a filesystem right, so a newer kernel is handled as 5. That is a real gap if some later ABI adds one, and kata
ctv9tracks re-checking the table. Declining Landlock above a known ABI was considered and rejected: it gives up a working sandbox on every future kernel to guard against a right that does not exist yet, and it would diverge fromlandlock_handled()inpkg-r/src/sandbox.c.Roots are resolved one level down, not just at the root itself. Landlock matches the hierarchy a path resolves to, so granting a package library whose entries are symlinks into a shared store, which is what Connect gives a deployed application, leaves every package in it unreadable. One level is the same depth
worker_init()resolves inpkg-r.The syscall numbers are behind an architecture allowlist. 444, 445 and 446 come from the table most architectures share, but mips offsets its whole table, where 444 is a different call. An unlisted architecture reports no Landlock and falls back.
A kernel with no Landlock returns
Nonerather than raising, which is what lets the user-namespace backend be chosen. A kernel that has it and then refuses a step raises, since that is a broken host rather than an old one.Verification: the cases that matter run a real interpreter on a real kernel and ask it to try things, using the host on Linux and a container elsewhere. Locally that was Landlock ABI 8, where reads and writes outside the granted roots come back as
PermissionError. The symlink case was confirmed to go red without the one-level resolution.