Skip to content

feat(py): the Landlock filesystem sandbox for the worker - #356

Draft
jat255 wants to merge 1 commit into
jat255/s87h-seccomp-network-blockfrom
jat255/z5gn-landlock-sandbox
Draft

feat(py): the Landlock filesystem sandbox for the worker#356
jat255 wants to merge 1 commit into
jat255/s87h-seccomp-network-blockfrom
jat255/z5gn-landlock-sandbox

Conversation

@jat255

@jat255 jat255 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

This PR adds _landlock.engage(), which restricts the worker to a set of read roots and a writable scratch directory using Landlock, reached through ctypes because 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 ctv9 tracks 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 from landlock_handled() in pkg-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 in pkg-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 None rather 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.

@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Sep 10, 2026
@jat255 jat255 added this to the py-M6: code execution milestone Sep 11, 2026
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
jat255 force-pushed the jat255/z5gn-landlock-sandbox branch from d54a489 to 50c69de Compare September 11, 2026 19:48
@jat255
jat255 changed the base branch from jat255/t7d4-sandbox-gate-rlimits to jat255/s87h-seccomp-network-block September 11, 2026 19:48
@jat255
jat255 added this pull request to stack #364 September 11, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-manual-review Agent-created work that needs a human review py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant