Skip to content

feat(py): the user-namespace and tmpfs sandbox for Linux - #357

Draft
jat255 wants to merge 1 commit into
jat255/z5gn-landlock-sandboxfrom
jat255/49te-linux-userns-sandbox
Draft

feat(py): the user-namespace and tmpfs sandbox for Linux#357
jat255 wants to merge 1 commit into
jat255/z5gn-landlock-sandboxfrom
jat255/49te-linux-userns-sandbox

Conversation

@jat255

@jat255 jat255 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

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 userns field of the capability probe, and adds the thread pinning that unshare(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. UsernsUnavailable means nothing has changed yet and this host cannot run this sandbox. UsernsError means the process already sits in a namespace whose ids or mounts are wrong, so it must die. pkg-r/src/sandbox.c reports 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 before unshare() 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 kata z5gn (#356), and the worker startup that would call either is kata bgk1. protection_mode() also still refuses every Linux host, because it wants seccomp and that probe is kata s87h. 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 both seccomp and landlock_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 kata dkev, 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.

@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review tests Related to testing or the test suite labels Sep 10, 2026
@jat255 jat255 added this to the py-M6: code execution milestone Sep 11, 2026
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
jat255 force-pushed the jat255/49te-linux-userns-sandbox branch from 79e000f to 7fdafcc Compare September 11, 2026 19:48
@jat255
jat255 changed the base branch from jat255/t7d4-sandbox-gate-rlimits to jat255/z5gn-landlock-sandbox 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 tests Related to testing or the test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant