feat(py): the user-namespace and tmpfs sandbox for Linux - #357
Draft
jat255 wants to merge 1 commit into
Draft
Conversation
The Linux sandbox for kernels without Landlock. The worker enters a new user and mount namespace, pivots into a fresh tmpfs root, binds back only the roots it was granted, closes descriptors that point elsewhere, and drops the capabilities the namespace handed it. Fills in the userns field of sandbox_capabilities(), which was reporting unavailable. The probe forks, because unshare(CLONE_NEWUSER) refuses a multi-threaded process and a fork child never is one. worker_env() gains single_thread, which pins OPENBLAS_NUM_THREADS and OMP_NUM_THREADS for the same reason, and needs_single_thread() decides when to ask for it. The behaviour tests run the real path and skip off Linux; they were driven in a container, where every one of them fails if the step it covers is removed.
jat255
force-pushed
the
jat255/49te-linux-userns-sandbox
branch
from
September 11, 2026 19:48
79e000f to
7fdafcc
Compare
jat255
changed the base branch from
jat255/t7d4-sandbox-gate-rlimits
to
jat255/z5gn-landlock-sandbox
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 the Linux user-namespace and tmpfs sandbox. The worker engages it on itself on a kernel that cannot offer Landlock. This PR also fills in the
usernsfield of the capability probe, and adds the thread pinning thatunshare(CLONE_NEWUSER)needs.kata
49te. Stacked on #356, at the top of the sandbox stack.Agent-written detail
engage()runs once in the worker and cannot be undone. It enters a new user and mount namespace, stops mount propagation, and pivots into a fresh tmpfs root. It then binds the granted roots back in from the old root, detaches the old root, and drops the capabilities the namespace gave it. It also closes any descriptor that points outside a granted root. A descriptor still reaches what it was opened on, whatever the mounts say.There are two exception types, and the split carries the contract.
UsernsUnavailablemeans nothing has changed yet and this host cannot run this sandbox.UsernsErrormeans the process already sits in a namespace whose ids or mounts are wrong, so it must die.pkg-r/src/sandbox.creports both cases as an errno. This is the one place the Python side is deliberately stricter, and it is why the architecture table is consulted beforeunshare()rather than at the pivot.available()probes in a fork.unshare(CLONE_NEWUSER)refuses a process with more than one thread, and a fork child always has one thread. A probe in place would therefore measure the thread count of the caller instead of the policy of the host.Nothing outside the tests calls
engage()yet. The choice between Landlock and this one is kataz5gn(#356), and the worker startup that would call either is katabgk1.protection_mode()also still refuses every Linux host, because it wants seccomp and that probe is katas87h. This change is therefore not yet visible to a user, and it fails closed while the rest of the stack lands.needs_single_thread()gates on bothseccompandlandlock_abi, so it only does its real work with #362 and #356 underneath it. That is why this sits at the top of the stack rather than beside them.The mountinfo parsing and the root-prefix match are internal, so they are not in
tests/shared/. The sandbox behaviour that both packages owe each other belongs to katadkev, which already owns that fixture.ruff, pyrefly and the full suite pass on macOS and on Linux in a container. The behaviour tests make the real syscalls. They skip on a host that denies user namespaces, which was checked under the default seccomp profile of Docker, where they skip and do not fail. Each step of
engage()was then removed in turn to confirm that a test fails without it. All eleven were caught.