Repository navigation
seccomp: park slow notification work off the loop - #340
Open
congwang-mk wants to merge 7 commits into
Open
congwang-mk wants to merge 7 commits into
congwang-mk wants to merge 7 commits into
Conversation
Chroot mode never moves the child's real root or cwd, yet the named unix gate checked the raw sun_path lexically and returned Continue, leaving the kernel to resolve it on the host. An absolute virtual path therefore reached the host's socket of that name (an escape where Landlock cannot gate unix connect), and a relative one was checked as if anchored at / but resolved against the cwd the child was exec'ed in, so ./svc.sock from a granted cwd was refused while tmp/x.sock from an ungranted one passed. Serve chroot connect/sendto/sendmsg/sendmmsg on-behalf like non-chroot: build the virtual path against the tracked virtual cwd, pin the inode inside the root (mount aware), and check write access on where it really sits. The trap now stays on under chroot even with Landlock ABI v9. Signed-off-by: Cong Wang <cwang@multikernel.io>
A deferred handler made a terminal decision: its Continue skipped every later handler, and the supervisor refused deferral on fork and exec because answering later would bypass fork accounting and the argv freeze. Deferrals beyond a fixed cap failed with EAGAIN even though each trapped task can only have one notification outstanding. Replace Defer with Park, which resumes where it was returned. A parked handler's Continue runs the next handler, any other result ends the chain, and the outcome then takes the same path as an inline decision. Fork accounting moves out of the chain to run once it settles on Continue, so no handler can park a fork after it was counted. All parks of a notification share one deadline, admission no longer fails, a panicked park still answers its caller, and settled parks are drained while notifications keep arriving. Signed-off-by: Cong Wang <cwang@multikernel.io>
Supervised connects ran on the notification loop after approval, so a slow peer or a full listener backlog stalled every other trapped syscall in the sandbox. Let a park carry a synchronous closure tagged with a lane. The closure runs on a process-wide runtime kept apart from the embedder's, whose shutdown would otherwise wait for a closure stuck on a peer. Each sandbox owns its slots per lane, so stalled work only delays its own sandbox, and a full lane queues instead of failing. External peers get their own lane so they cannot take the slots local filesystem work needs. A closure runs to completion and keeps its slot and captures even after its waiter times out. Both supervised connects now run there. Signed-off-by: Cong Wang <cwang@multikernel.io>
Parked work keeps running while the loop validates an execve's argv under the freeze. A write it makes into a CLONE_VM peer's memory can land on that argv after validation and before the kernel copies it, and the deferred sendmmsg completion already wrote msg_len that way. Give each sandbox a gate. Parked futures and native closures run with it installed, write_child_mem and write_child_mem_force hold its shared side for each single write, and the exec freeze holds the exclusive side until the freeze is released. Writes are the only access that can change argv and each holds the gate for one copy, so an exec waits microseconds and never fails because unrelated work is still running. Signed-off-by: Cong Wang <cwang@multikernel.io>
Chroot path resolution and COW copy-ups do synchronous filesystem I/O while the notification loop waits, so one large copy or slow lookup held up every trapped syscall in the sandbox. Make these handlers plain synchronous functions and park them as native work on the local lane. Peers and callbacks use the other lane, so they cannot take the slots filesystem work needs. A native closure always runs to completion, so a copy keeps main's shape of copying outside the state lock and recording its digest or rolling back its quota after, even when its waiter timed out. A failed pre-copy now fails the write with EIO instead of continuing onto the lower file. Signed-off-by: Cong Wang <cwang@multikernel.io>
A native closure runs to completion after its waiter gives up, so it can outlive the supervisor and still use the listener's raw fd number for child-memory access. Once the supervisor closed the listener that number could be reused by another sandbox's listener. Share the listener between the supervisor and the native closures it parks, so its fd stays open until the last closure returns. Signed-off-by: Cong Wang <cwang@multikernel.io>
A C callback was awaited on the notification loop unless the caller flagged it deferred, and a deferred callback made a terminal decision that skipped later handlers and was refused on fork and exec. Callers had to choose between stalling every trapped syscall and changing what their Continue meant. Park every callback on the external lane, where Continue resumes the chain like any other park, and remove sandlock_handler_set_deferred. A callback runs to completion after its syscall times out, but the caller's userdata may only be borrowed until the run returns, so each run holds a guard per handler: dropping it waits for running callbacks and releases the container, and callbacks still queued fail with EIO instead of reaching freed userdata. Python async handlers no longer need a flag, and a sync handler no longer stalls the supervisor. Signed-off-by: Cong Wang <cwang@multikernel.io>
This branch has not been deployed
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.
Supersedes #337. Its chroot unix socket fix is the first commit here unchanged; the rest replaces its off-loop dispatch with a smaller model.
Problem
Supervised connects, FFI callbacks and chroot/COW filesystem handlers ran on the notification loop, so one slow peer, callback or copy stalled every trapped syscall in the sandbox. The existing escape hatch,
NotifAction::Defer, made a terminal decision: itsContinueskipped later handlers, it was refused on fork and exec, and it failed withEAGAINpast a fixed cap.Design
ParkreplacesDeferand resumes where it was returned. A parked handler'sContinueruns the next handler; any other result ends the chain. The outcome then takes the same path as an inline decision (pinned opens, fork accounting, policy event, argv freeze), so parking on fork or exec is safe. All parks of a notification share one deadline.NotifAction::native(lane, closure)in per-sandbox lane slots (Localfor filesystem work,Externalfor peers and callbacks) on a process-wide runtime kept apart from the embedder's. A full lane queues; it never returnsEAGAIN.write_child_memandwrite_child_mem_forcehold the shared side of a per-sandbox gate for each write made by parked work; the exec freeze holds the exclusive side until the kernel has copied argv. Exec waits microseconds instead of failing, and no call site has to remember a guard.Continue, so no handler can park a fork after it was counted, and a failed response rolls the slot back.EIOinstead of continuing onto the lower file.Externalwork.sandlock_handler_set_deferredis removed (pre-1.0 hard break). A per-handler run guard waits for running callbacks before the FFI run returns, and callbacks still queued fail withEIOinstead of touching freed userdata. Python handlers, sync or async, run off the loop with no flag.Behavior changes
Continuenow hands the syscall to the next handler instead of ending the chain.sandlock_handler_set_deferredis gone from the C ABI and the Python SDK.Not measured
Every chroot and COW syscall now takes a hop to a worker thread and back to the loop. Hot-path latency for chroot
open/stathas not been benchmarked.Test plan
continue_()reaches the next handler on the syscall🤖 Generated with Claude Code