The envelope is a process namespace
Amended 2026-10-19 by await-is-the-block-run-here:
serviceis deleted, sospawnanddetachare the verbs for outliving a command.
A grant’s envelope confines process identity and lifetime, not only the
filesystem and network — on every projection, wherever the host can build the
namespace, and it says when it cannot. What grant means for ps, kill,
orphans and detach changes accordingly — and so does what the filesystem
inside it says about the host.
Why
- Under
grant [fs: [read: [$dir]]]a child could read every host process’scmdlineandenviron,killevery same-uid process, andshmatany same-uid segment. A grant that withholds~/.awsand hands overps -efis not the mechanism it claims to be. - Process identity is authority, and an envelope that leaves it ambient is a view that shows the whole room through one wall (two-enforcers).
The process model, corrected
bwrap never execs the payload in place: its monitor clones a namespace init,
and the init forks the payload. --new-session’s setsid runs in the payload,
so the payload’s pid is the session and the group of everything inside. Every
ladder used to signal the monitor, which died and took the payload down by
PDEATHSIG with zero grace — Ctrl-C hard-killed a confined command, a trap
never ran.
Decision
- Namespaces on every launch, keyed on nothing in the grant:
--unshare-ipc --unshare-utsunconditionally,--unshare-pidiff the host can mount a fresh procfs and--unshare-cgroupiff the kernel builds the namespace (HostEnvelope::private_pids,::private_cgroup, probed once per process).--proc /procon both projections; its meaning follows bwrap’s own rule — a fresh table with the namespace, a bind of the host’s without — so/proc/selfis always the payload’s own. - A pseudo-filesystem inside the envelope describes the envelope; one that
cannot is a host bind like any other under
Restricted, present only where something needs it. sysfs is its mounter’s: a bound/syslists the host’s interfaces under--unshare-netand names the machine (class/dmi,bus,block). bwrap has no sysfs op, sodefault_ro_bindsnarrows/sysas it narrows/etc—devices/system/cpu,kernel/mm/transparent_hugepage, what sizes a program — and a grant that wants the rest reads/sysby name. Not keyed onnet: the identifiers leak undernet: truetoo.Unrestrictedis the host’s filesystem and keeps the host’s/sys. /sys/fs/cgroupis the tree the payload’s own/proc/self/cgroupnames. Under the cgroup namespace that file reads0::/, and a runtime joining it onto the host’s tree — Rust’savailable_parallelism, the JVM, Go — reads the root’s limits, which are none; sorender_cgroupre-roots the tree on ral’s own cgroup, on both projections, over the projection’s binds: what a fresh cgroup2 mount inside the namespace would show, built from the op bwrap has. Without the namespace the host’s tree is the true one and is bound as it is; cgroup v1 names no single tree and binds nothing.- The ladder addresses the payload’s session, never the supervisor. A
Keptlaunch passes--info-fdand reads backchild-pid; that pid is the group.kill(-P, grace)reaches the payload — a namespace init drops every signal from outside butSIGKILLandSIGSTOP— andkill(-P, SIGKILL)ends the init and with it the namespace. The monitor leads an inert group nobody signals. An enveloped launch isNewLeaderwhatever was asked, the foreground is never handed to it, and a confined pipeline stage’s group is addressed beside the pipeline’s (one-delivery-path). - bwrap’s
128 + nis the payload’s death byn, attributed to the teardown that sentnexactly as aSignaled(n)is. Ownershipdecides the two ties — death (--die-with-parent) and address (--info-fd) — and nothing about what the envelope is. ASurrenderedsurvivor keeps every namespace: its init outlives the session, so a daemon runs confined until it exits; the receipt’spidnames the monitor, dead the moment a self-daemonizing program hands it back; the survivor’sgetpid()is namespace-local; from inside any later grant it is invisible — the frame that cannot name it to widen it cannot name it to kill it. AKeptpayload’s orphans die with the envelope:spawn,serviceanddetachare the verbs for outliving a command.- A host that cannot build the namespace is not refused. A grant
promises what it names —
fs,net,exec— and nothing in it names process reach; the namespace is an invariant of being under an envelope, like--die-with-parentand the seccomp blocklist, applied where the host allows. Container runtimes that mask/proc(crun, runc, Docker) leave locked mounts the kernel will not let a fresh procfs cover in a user namespace; there the payload sees the container’s own table, and--privilegedis the flag measured to lift it (unmask=ALLdoes not, on rootless podman); SPEC §12 states the gap. Where--devis refused,/devis built by hand over the host’s/dev/pts, likewise stated and not refused. - The datum is the host’s, not the launch’s. One probed value per process, read where host facts are read; no mark per launch, since a constant carries no information.
- Nothing on Linux enters a sandbox from inside.
--sandbox-projectionis refused there as on Windows; a nested envelope is the one thing that must not happen.
Rejected
- Fail closed where the namespace cannot be built: kills every confined launch in every rootless container for a property no grant asked for.
--tmpfs /procas the fallback: a different world, not a tighter one —/proc/selfgone, every runtime that reads/proc/self/exebroken silently.- A process-reach axis on the projection: it would exist only to have something to refuse, and its in-process half is already total. The day a shared process view is wanted, that is a widening to add.
- Dropping
--new-session: leaves the monitor in the signalled group and reopensTIOCSTI(CVE-2017-5226). SIG_IGNbefore exec, so the monitor survives the group signal: inherited by the payload — thenohupdisease under every grant.--as-pid-1: the monitor remains, and a payload with the default disposition silently drops the grace signal.- One namespace per session (
--pidns): needs a session-lifetime init and an answer to which frames share a namespace, which the lattice does not give. Per-envelope is the meet-safe default; sharing is a later widening. --unshare-all --share-net: its manual defines it as “currently equivalent with …”; the argv should say what it means.--unshare-cgroup-try: the re-rooted tree is right exactly when the namespace exists, so the flag follows the probe as--unshare-piddoes.--tmpfs /sys/class/net: a symlink farm — the directories stay reachable at/sys/devices/**/net/*; a wall built from a list of doors.- A trampoline that mounts sysfs from inside: mount authority in the payload’s hands unmounts every deny mask (now killed by the deny-set).
Left open, deliberately
- A confined interactive command has no controlling terminal: Ctrl-C reaches
ral and the ladder delivers a real
SIGINT, butopen("/dev/tty")fails andSIGWINCHnever arrives. The fix — a trampoline inside the envelope thatsetpgids,SO_PASSCREDfor its pid,tcsetpgrp— is a second design;TIOCSTIis already refused by the seccomp deny-set. - Rootless containers still refuse the read-only rebind of their locked
/etc/hostsand/etc/resolv.conf, soRestricteddoes not launch there. - Under
grant [net: false]alone the filesystem is the host’s,/sys/class/netincluded:netgoverns sockets, not the filesystem’s description of the host.
Pinned by tests that spawn the envelope (sandbox::linux::tests): the grace
signal reaching the payload rather than the monitor, no host pid nameable
inside, /proc/self the payload’s own on both projections, /sys/fs/cgroup
the inode of ral’s own cgroup with /sys/class/net absent under Restricted,
and a confined pipeline stage’s trap running (ral/tests/pipeline.rs); the
/sys binds and their order by
sys_is_narrowed_to_what_sizes_a_program_and_the_cgroup_tree_is_the_payloads. See
grant, capability-enforcement,
docs/SPEC.md §12.6 and §12.11.