Skip to content

control: use SEM_UNDO for sandbox name ownership and discovery - #256

Merged
congwang-mk merged 3 commits into
mainfrom
control-sem-undo
Sep 22, 2026
Merged

congwang-mk merged 3 commits into
mainfrom
control-sem-undo

Conversation

@congwang-mk

@congwang-mk congwang-mk commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

An abstract socket name remains bound while any inherited descriptor refers to it. A host process that forks while a sandbox is running can therefore keep the sandbox name occupied after the supervisor releases it or exits.

Reserve names in a per-user System V semaphore registry with SEM_UNDO. Ordinary forks do not inherit the undo adjustment, so supervisor exit releases the reservation even if a host child keeps every inherited descriptor. Normal cleanup and failed or cancelled startup release it explicitly. Each instance gets a random request socket address, allowing immediate name reuse despite lingering descriptors.

The registry also replaces the process-group socket. After setpgid and before confinement, the child stamps a dedicated semaphore; GETPID supplies the kernel-recorded PID. An instance-token check under the registry transaction lock prevents a delayed child from stamping a reused slot. The request socket remains for control commands and authenticates the supervisor with SO_PEERCRED.

Supersedes #244. This removes the need to park a descriptor in SCM_RIGHTS, the associated in-flight descriptor limit, and the lifetime backlog budget for PID lookups. Discovery reads the registry instead of /proc/net/unix. No dedicated control thread, helper process, filesystem path, or io_uring is required.

The registry grows in segments of 4,096 entries with no compiled-in total sandbox limit. Each segment has one set of 8,196 semaphores plus shared metadata for names, tokens and index links. A root directory has 65,536 hash buckets; collisions form chains across segments, so the bucket count is not a capacity limit. Name lookup and reservation scan the matching bucket. A small insertion journal recovers interrupted index publication, and segment identity checks reject IPC key collisions before modifying reservations.

Registry objects persist for reuse. Inactive entries are reused within their hash bucket, so storage reflects historical demand per bucket. Host IPC and memory limits bound growth; exhaustion and registry errors fail creation. Transaction waits are bounded to two seconds. Clients must share the relevant IPC and network namespaces, and this versioned protocol does not discover older instances. A host explicitly using CLONE_SYSVSEM can share undo state and delay automatic exit cleanup.

Default seccomp confinement blocks semaphore and shared-memory access even when the workload knows their identifiers, in both supervised and unsupervised modes. Metadata attachments are temporary and marked MADV_DONTFORK while holding the sandbox fork lock, protecting in-process entrypoints as well as applications that exec. Explicitly allowing sysv_ipc restores same-UID access. These boundaries and the registry lifecycle are documented in docs/architecture.md.

Validation covered 832 core unit tests, 488 integration tests, and 102 CLI tests. The full run exposed a missing-name diagnostic regression; after restoring the existing message, all 30 control integration tests passed on rerun. Coverage includes 40,000 simultaneous reservations across growing segments, indexed lookup and reuse, duplicate detection past inactive bucket entries, killed publishers at three index publication stages, segment identity mismatch, lingering host children, supervisor death, failed and cancelled startup, stale child publication, repeated PID lookups, default IPC access denial, and mapping non-inheritance.

An inherited abstract socket keeps its name bound after the supervisor
exits. A host that forks while a sandbox is running can therefore leave
its name occupied until the last child closes the descriptor, even
though the sandbox itself is gone.

Reserve names in a per-user System V semaphore registry with SEM_UNDO.
The kernel releases the reservation on supervisor exit, and ordinary
forks do not inherit the undo adjustment. Give each instance random
socket addresses so that inherited descriptors cannot block name reuse.
Discovery reads the active reservations, while requests still use the
abstract sockets and authenticate the supervisor with SO_PEERCRED.

The registry is diskless and reuses a persistent set with 256 slots.
Release claims on normal cleanup and failed or cancelled startup too.
Registry exhaustion and transaction failures must fail creation rather
than silently leave a sandbox outside control discovery.

Signed-off-by: Cong Wang <cwang@multikernel.io>
The process-group socket only supplies the child's kernel-recorded pid.
Now that discovery has a semaphore registry, keeping a second socket
for that identity also keeps a listen backlog that PID lookups can fill
when the control loop is not running.

Have the child stamp a dedicated semaphore after setpgid and before
confinement. GETPID supplies its identity without a socket connection.
Check the instance token in the same atomic semop so that a delayed
child cannot stamp a reused slot. Leave the supervisor's ownership
semaphore untouched and create no undo adjustment in the child.

Keep the request socket for control commands. The default seccomp
policy denies SysV IPC, so the confined workload cannot access the
registry even with its identifier. Test reads, writes and removal in
both supervised and unsupervised modes, and document that explicitly
allowing SysV IPC restores same-UID access.

Signed-off-by: Cong Wang <cwang@multikernel.io>
Encoding names and tokens in semaphore values limits the registry to
256 live names. Moving metadata out of those values helps, but a fixed
registry still caps sandbox density at the size of its semaphore sets.

Store names, instance tokens and index links in shared memory, and grow
in segments of 4,096 entries as demand increases. Each segment uses one
set of 8,196 semaphores for ownership and child pid stamps. A shared hash
index directs name lookup and reservation to one bucket, avoiding a
scan of every sandbox. Inactive entries are reused within their bucket;
there is no compiled-in total sandbox limit.

Serialize metadata changes and child generation checks with a root
SEM_UNDO lock. Journal new index entries so that a later process can
recover publication interrupted by a crash. Segment headers identify
the root and segment ordinal before their semaphore sets are used, so
IPC key collisions fail without altering another segment's owners.

Attach shared memory only during registry operations. Mark attachments
MADV_DONTFORK under the sandbox fork lock, and detach the child's own
attachment before confinement, including for entrypoints without exec.
Default seccomp still denies access to every registry IPC object.

Exercise 40,000 simultaneous reservations, indexed lookup and reuse,
duplicates behind inactive bucket entries, and recovery from a killed
index publisher at each publication stage. The registry remains
diskless; allocated objects persist for reuse, with growth bounded by
host IPC and memory limits.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
congwang-mk merged commit fc962b6 into main Sep 22, 2026
17 checks passed
@congwang-mk
congwang-mk deleted the control-sem-undo branch September 22, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant