Skip to main content

ferroday_cage/mechanism/
mod.rs

1//! The sandbox launch mechanism: fork, the launch-stage pipes, and the
2//! supervisor plumbing.
3//!
4//! This module is one of the crate's two homes of `unsafe` (the other is
5//! the network stack's tap layer, `crate::netstack::tap`). The unsafe
6//! surface here is the process-creation and exec boundary — `kernel_fork`,
7//! `unshare_unsafe`, `execve`, the signal-state calls, and the descriptor
8//! sweep — each with its invariant documented at the call site, and
9//! everything after the fork runs in a child process under the constraints
10//! documented in [`child`].
11//! All syscalls go through rustix's `linux_raw` backend, which issues them
12//! with inline assembly and never touches the libc `errno` slot, the
13//! allocator, or a lock — which is what keeps the post-fork window
14//! async-signal-safe. rustix selects that backend by target and build
15//! configuration, not by the mere fact of depending on rustix, so a
16//! compile-time guard (below) fails the build on any configuration where
17//! rustix would fall back to its libc backend instead.
18//!
19//! A launch forks a short-lived stage that creates the namespaces and forks
20//! the sandbox's long-lived processes (see [`child`] for the full process
21//! tree). Four channels connect the caller to them, all created here before
22//! the fork:
23//!
24//! - the **report pipe** carries a failed setup step as one 12-byte record —
25//!   step discriminant, errno, detail index, little-endian. A successful
26//!   `execve` closes the last write end, so end-of-file means the command is
27//!   running.
28//! - the **status pipe** carries the command's wait status as one 8-byte
29//!   record — kind, value, little-endian — written by the supervisor when
30//!   the command terminates.
31//! - the **control socket** (a seqpacket pair) first delivers the
32//!   supervisor's pidfd and host pid to the caller, then carries control
33//!   requests toward the supervisor; its hangup is the caller-exit signal
34//!   for [`stop_with_caller`].
35//! - the supervisor's **pidfd** is the kill handle.
36//!
37//! [`stop_with_caller`]: crate::CageBuilder::stop_with_caller
38
39#![allow(unsafe_code)]
40
41// The post-fork stages are async-signal-safe only because rustix issues
42// syscalls through its `linux_raw` backend (inline assembly; no libc `errno`
43// slot, allocator, or locks). rustix falls back to its libc backend on targets
44// it has no `linux_raw` support for, on the x32 and arm64-ILP32 ABIs (a 32-bit
45// pointer on an otherwise-64-bit arch), under Miri, and when `rustix_use_libc`
46// or `rustix_no_linux_raw` is set via `--cfg`. Any of those would silently
47// invalidate the discipline documented in `child`, so fail the build instead.
48// The default `linux_raw` architecture set is x86_64, x86, aarch64, arm, and
49// riscv64; architectures rustix drives only through libc by default (powerpc,
50// s390x, mips) are excluded here by omission. The one selector this cannot
51// observe is rustix's own `use-libc` cargo feature, a deliberate opt-out that
52// no default build enables.
53#[cfg(any(
54    rustix_use_libc,
55    rustix_no_linux_raw,
56    miri,
57    not(target_os = "linux"),
58    not(any(
59        target_arch = "x86_64",
60        target_arch = "x86",
61        target_arch = "aarch64",
62        target_arch = "arm",
63        target_arch = "riscv64",
64    )),
65    all(target_arch = "x86_64", target_pointer_width = "32"),
66    all(target_arch = "aarch64", target_pointer_width = "32"),
67))]
68compile_error!(
69    "ferroday-cage requires rustix's linux_raw backend for post-fork \
70     async-signal-safety, but this target or build configuration selects \
71     rustix's libc backend. See the mechanism module documentation."
72);
73
74pub(crate) mod bounds;
75mod child;
76mod copyin;
77#[cfg(feature = "tarball")]
78mod export;
79pub(crate) mod frame;
80#[cfg(feature = "hardening")]
81mod harden;
82mod overlay;
83mod remove;
84
85#[cfg(feature = "hardening")]
86pub(crate) use harden::probe_landlock_abi;
87
88pub(crate) use child::CONTROL_TERMINATE;
89// The network stack's tap helper is the one forked child outside this module,
90// and it sweeps like every child inside it.
91#[cfg(feature = "netstack")]
92pub(crate) use child::sweep_fds_keeping;
93pub(crate) use copyin::{
94    COPY_DIR, COPY_END, COPY_FIFO, COPY_FILE, COPY_LEAVE, COPY_LINK, COPY_SYMLINK, CopyFailure,
95    start_copy_in,
96};
97#[cfg(feature = "tarball")]
98pub(crate) use export::{
99    ExportStep, FRAME_DIR, FRAME_END, FRAME_ERROR, FRAME_FILE, FRAME_LEAVE, FRAME_SYMLINK,
100    StartError, kill_export, start_export,
101};
102pub(crate) use overlay::{OverlayProbe, probe_overlay_mount, xattr_unsupported};
103pub(crate) use remove::{RemoveFailure, remove_tree_mapped};
104
105use std::ffi::{CStr, CString};
106use std::mem::MaybeUninit;
107use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
108use std::path::PathBuf;
109use std::sync::Arc;
110
111use rustix::fs::RawMode;
112use rustix::io::Errno;
113use rustix::mount::MountFlags;
114use rustix::net::{
115    AddressFamily, RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, SocketFlags, SocketType,
116};
117use rustix::pipe::{self, PipeFlags};
118use rustix::process::Signal;
119use rustix::process::{self, Pid, PidfdFlags, WaitId, WaitIdOptions, WaitOptions, WaitStatus};
120use rustix::runtime::{self, Fork};
121use rustix::thread::UnshareFlags;
122
123use crate::error::{Error, SetupStep};
124use crate::host;
125use crate::idmap::{IdMapper, ResolvedMap};
126use crate::observer::Progress;
127use crate::status::ExitStatus;
128use crate::terminal::Terminal;
129
130/// What the launch stages build around the command.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub(crate) enum Confinement {
133    /// The container: namespaces, identity maps, the mount profile, and the
134    /// pivot into the rootfs.
135    Container,
136    /// The restriction fallback: a plain child process on the host, confined
137    /// by the hardening plan alone. No namespaces are created, and the
138    /// namespace and mount fields of the plan are empty and unused.
139    #[cfg(feature = "hardening")]
140    Restriction,
141}
142
143/// How the launch establishes the sandbox's identity map.
144///
145/// The tier is a property of the plan, decided at build time: the map files
146/// are write-once, so whichever party applies the map must apply all of it,
147/// once.
148#[derive(Debug, Clone)]
149pub(crate) enum IdentityPlan {
150    /// The single-identity map, written by stage A in process under the
151    /// kernel's unprivileged one-id exception. No gate.
152    Single,
153    /// A range map, written from outside the new namespace by the resolved
154    /// delegate while stage A blocks at the map gate.
155    Ranged {
156        /// The delegate that applies the map.
157        mapper: Arc<dyn IdMapper>,
158        /// The extents, resolved at build time.
159        map: ResolvedMap,
160    },
161}
162
163impl IdentityPlan {
164    /// Whether this plan's map is written by a delegate through the map
165    /// gate.
166    pub(crate) fn is_ranged(&self) -> bool {
167        matches!(self, IdentityPlan::Ranged { .. })
168    }
169}
170
171/// How the command's nested user namespace gets its identity map, and the
172/// procfs paths that way of getting it names.
173///
174/// The tier mirrors the sandbox's own: the single-identity map is one extent of
175/// one id and the command writes it in process, and a range map is written from
176/// outside the nested namespace by a delegate — here a delegate inside the
177/// sandbox, because the party that writes it must be root of the namespace the
178/// nested one is a child of.
179///
180/// Each variant carries the paths its own route needs and no others, so a
181/// configuration that names none — a delegated map whose delegate is outside
182/// the sandbox — records that rather than a path nothing opens.
183#[derive(Debug, Clone)]
184pub(crate) enum NestedMap {
185    /// The command writes its own map, under the kernel's unprivileged one-id
186    /// exception: `setgroups` is denied first, which is what permits the write.
187    ///
188    /// The files are reached by path, after the pivot, so they are named
189    /// through the procfs the mount profile establishes.
190    SelfWritten {
191        /// `/proc/self/setgroups` as it is named inside the sandbox.
192        setgroups_path: CString,
193        /// `/proc/self/gid_map` as it is named inside the sandbox.
194        gid_map_path: CString,
195        /// `/proc/self/uid_map` as it is named inside the sandbox.
196        uid_map_path: CString,
197    },
198    /// A delegate — the process that forked the command — writes the map while
199    /// the command blocks at a gate. `setgroups` stays at `allow`, so the
200    /// supplementary groups a run-as identity names still work.
201    Delegated {
202        /// The procfs mount target, rootfs-relative, that a delegate inside the
203        /// sandbox opens during setup and writes the command's map files
204        /// through.
205        ///
206        /// `None` where the sandbox has no PID namespace: the delegate is then
207        /// the launch stage, which is outside and opens the host's `/proc`
208        /// before the pivot, so the profile's own procfs is never consulted and
209        /// a profile that mounts none still works.
210        procfs_target: Option<CString>,
211    },
212}
213
214/// The nested user namespace the command enters before the hardening layer,
215/// composed and frozen at build time.
216///
217/// A read-only bind is established by the sandbox setup inside the user
218/// namespace that setup created, and the kernel locks a mount's flags only when
219/// it copies the mount tree *into* a new user namespace. Root of the namespace
220/// that owns a mount may therefore remount it read-write — so the command, root
221/// of its own namespace, could clear the very flag the bind was for. Entering a
222/// nested user namespace has the kernel do the copy, and with it the locking:
223/// past the entry no mount the command inherited can have a restriction lifted.
224///
225/// The map is the identity reflection of the sandbox's own (see
226/// [`reflect_inside`](crate::idmap::reflect_inside)), so the command's view of
227/// itself is unchanged one namespace deeper.
228///
229/// Present for every container; `None` for the restriction fallback, which
230/// creates no user namespace to nest inside.
231#[derive(Debug, Clone)]
232pub(crate) struct NestedPlan {
233    /// Who writes the map, and the procfs paths that route names.
234    pub(crate) map: NestedMap,
235    /// The `uid_map` content, formatted at build time.
236    pub(crate) uid_lines: Vec<u8>,
237    /// The `gid_map` content, formatted at build time.
238    pub(crate) gid_lines: Vec<u8>,
239}
240
241/// How an overlay-rooted cage's root is assembled.
242///
243/// The base is the read-only lower and [`rootfs_path`](LaunchPlan::rootfs_path)
244/// both — the overlay is mounted over the base's own directory — so this
245/// carries only the preformatted mount options naming the lower, the disposable
246/// upper, and its work directory. Present only when the caller requested an
247/// overlay root; a plain cage binds its rootfs onto itself instead.
248#[derive(Debug, Clone)]
249pub(crate) struct OverlayPlan {
250    /// The overlay mount options, preformatted at build time:
251    /// `lowerdir=<base>,upperdir=<upper>,workdir=<work>,userxattr`.
252    pub(crate) options: CString,
253    /// The resolved lower layers, first (the base the overlay mounts over)
254    /// first. Kept beside the formatted options so `resolved_inputs` can report
255    /// the stack as a structure rather than leave a caller to parse it back out
256    /// of the option string.
257    pub(crate) lower: Vec<PathBuf>,
258    /// The resolved upper layer, where the sandbox's writes land.
259    pub(crate) upper: PathBuf,
260    /// The resolved work directory the overlay requires beside the upper.
261    pub(crate) work: PathBuf,
262}
263
264/// How the command stage wires one standard stream, frozen at build time.
265///
266/// The descriptor form holds the caller's descriptor for the plan's lifetime,
267/// shared so a clonable plan can launch repeatedly against it.
268#[derive(Debug, Clone, Default)]
269pub(crate) enum StdioPlan {
270    /// Leave the stream's descriptor as the launch inherited it.
271    #[default]
272    Inherit,
273    /// Redirect the stream to `/dev/null`.
274    Null,
275    /// Redirect the stream to a caller-supplied descriptor.
276    Fd(Arc<OwnedFd>),
277}
278
279impl StdioPlan {
280    /// Lowers a public disposition into the plan's own form.
281    pub(crate) fn of(stdio: &crate::Stdio) -> StdioPlan {
282        match stdio {
283            crate::Stdio::Inherit => StdioPlan::Inherit,
284            crate::Stdio::Null => StdioPlan::Null,
285            crate::Stdio::Fd(fd) => StdioPlan::Fd(Arc::clone(fd)),
286        }
287    }
288
289    /// The caller's descriptor, for the one variant that names one.
290    ///
291    /// The launch keeps these through the descriptor sweep and wires them onto
292    /// the command's standard streams; every other variant names no descriptor
293    /// of the caller's.
294    pub(crate) fn caller_fd(&self) -> Option<BorrowedFd<'_>> {
295        match self {
296            StdioPlan::Fd(fd) => Some(fd.as_fd()),
297            StdioPlan::Inherit | StdioPlan::Null => None,
298        }
299    }
300
301    /// Whether this disposition names a destination of its own, which a
302    /// launch-time attachment — capture pipes — would contradict.
303    ///
304    /// [`Inherit`](Self::Inherit) states no destination, so it is not a
305    /// contradiction: the launch is free to supply one.
306    pub(crate) fn names_a_destination(&self) -> bool {
307        !matches!(self, StdioPlan::Inherit)
308    }
309}
310
311/// One frozen resource limit, in the kernel's own terms.
312///
313/// Validated and translated at build time, so the command stage only replays
314/// a `prlimit64` per entry.
315#[derive(Debug, Clone, Copy)]
316pub(crate) struct RlimitPlan {
317    /// The resource as the caller named it.
318    ///
319    /// Carried beside the kernel's own, rather than recovered from it, so that
320    /// [`ResolvedInputs`](crate::ResolvedInputs) reports the resource a
321    /// provenance record is asked about instead of searching for one that
322    /// translates to the same kernel number. It joins `op_labels`, `exec_label`
323    /// and `rlimit_labels` as caller-side data the sandbox stages never read.
324    pub(crate) resource: crate::Resource,
325    /// The kernel resource the limit is applied against.
326    pub(crate) kernel: rustix::process::Resource,
327    /// The soft and hard values, with `None` meaning `RLIM_INFINITY`.
328    pub(crate) limit: rustix::process::Rlimit,
329}
330
331/// The frozen non-root identity the command switches to before `execve`.
332///
333/// The ids are sandbox-inside ids, validated against the identity map at
334/// build time, so the switch itself replays frozen values.
335#[derive(Debug, Clone)]
336pub(crate) struct RunAsPlan {
337    /// The uid the command runs as.
338    pub(crate) uid: rustix::thread::Uid,
339    /// The gid the command runs as.
340    pub(crate) gid: rustix::thread::Gid,
341    /// The supplementary groups, set verbatim — an empty list clears the
342    /// caller's inherited groups.
343    pub(crate) groups: Vec<rustix::thread::Gid>,
344}
345
346/// The mount targets a launch has to create as empty files in the root, and the
347/// writable layer they are created in.
348///
349/// A file bind needs its target to exist before anything can be mounted onto it,
350/// so a root shipping none gets an empty file created. Left behind, that file is
351/// content the root did not have — a resolver configuration, an empty binary —
352/// which an export carries into whatever is made of the tree. The caller side
353/// removes each one after the launch instead: the sandbox owns what it creates.
354///
355/// Only the targets the caller side can name are listed: one under an earlier
356/// mount is created inside that mount rather than in the root, and one the root
357/// already ships — an entry of any kind at the path — is not the launch's to
358/// remove.
359///
360/// The paths are held root-relative, with the root named once, so the removal
361/// can resolve them the way the sandbox stage resolves its mount targets:
362/// against a descriptor for the root, with `RESOLVE_IN_ROOT`. Joined into
363/// absolute host paths instead, a rootfs-internal symbolic link — which an
364/// untrusted unpacked tree is entitled to contain — would resolve against the
365/// *host's* root on this side of the fork, and the removal could land outside
366/// the rootfs entirely.
367///
368/// Caller-side only, and never touched by the sandbox stages: inside the sandbox
369/// each path is under its bind, where unlinking it is `EBUSY`.
370#[derive(Debug, Clone, Default)]
371pub(crate) struct ManagedPlaceholders {
372    /// The root's writable layer — the rootfs itself, or an overlay's upper —
373    /// which is where the launch creates the targets below. Canonical, so a root
374    /// the caller named relatively does not re-resolve against whatever
375    /// directory the process happens to be in when the handle drops. Empty when
376    /// there are no placeholders.
377    pub(crate) root: PathBuf,
378    /// The targets, as paths relative to [`root`](Self::root).
379    pub(crate) paths: Vec<PathBuf>,
380}
381
382impl ManagedPlaceholders {
383    /// Whether the launch creates no mount target the caller side has to remove.
384    pub(crate) fn is_empty(&self) -> bool {
385        self.paths.is_empty()
386    }
387}
388
389/// The frozen launch plan: everything the sandbox stages need, marshaled by
390/// [`CageBuilder::build`] so that the post-fork windows perform no fallible
391/// or allocating preparation of their own.
392///
393/// [`CageBuilder::build`]: crate::CageBuilder::build
394#[derive(Debug, Clone)]
395pub(crate) struct LaunchPlan {
396    /// What the stages build around the command: the full container, or the
397    /// restriction fallback's plain confined process.
398    pub(crate) confinement: Confinement,
399    /// How the sandbox's identity map is established; unused in a
400    /// restriction, which creates no user namespace.
401    pub(crate) identity: IdentityPlan,
402    /// The nested user namespace the command enters before the hardening
403    /// layer, which is what locks the flags of every mount it inherited.
404    /// `None` in a restriction, which creates no namespace to nest inside.
405    pub(crate) nested: Option<NestedPlan>,
406    /// The identity the command switches to before `execve`; `None` leaves
407    /// it running as root inside. Always `None` in a restriction.
408    pub(crate) run_as: Option<RunAsPlan>,
409    /// The command path, executed after the root swap in a container and
410    /// against the host filesystem in a restriction. When
411    /// [`program_search`](Self::program_search) is non-empty this is the bare
412    /// command name, used as `argv[0]`, and the search list holds the paths to
413    /// try.
414    pub(crate) program: CString,
415    /// Candidate absolute paths to `execve` in order, resolved from the
416    /// sandbox's `PATH` when the caller opts into path lookup for a command
417    /// with no slash. Empty for the ordinary case, where
418    /// [`program`](Self::program) is executed directly.
419    pub(crate) program_search: Vec<CString>,
420    /// The command's arguments, excluding the conventional `argv[0]`.
421    pub(crate) args: Vec<CString>,
422    /// The command's complete environment, as frozen `NAME=value` entries.
423    pub(crate) env: Vec<CString>,
424    /// The canonicalized rootfs path, validated at build time; empty and
425    /// unused in a restriction, which swaps no root.
426    ///
427    /// The sandbox stage resolves this path anew inside its own mount
428    /// namespace: a descriptor opened before `unshare` still references the
429    /// original namespace's mounts, and `mount` refuses bind sources and
430    /// targets that are not attached in the caller's namespace.
431    pub(crate) rootfs_path: CString,
432    /// When set, root the cage on an overlay of the rootfs rather than the
433    /// rootfs directly: [`rootfs_path`](Self::rootfs_path) is the read-only
434    /// lower, and the sandbox stage mounts the overlay over it before the pivot
435    /// so writes land in a disposable upper. `None` for a plain rootfs.
436    pub(crate) overlay: Option<OverlayPlan>,
437    /// The namespaces the launch stage unshares into.
438    pub(crate) unshare: UnshareFlags,
439    /// Whether the sandbox gets its own PID namespace, and with it the
440    /// library init and namespace-wide teardown.
441    pub(crate) pid_namespace: bool,
442    /// How the command's standard input is wired.
443    pub(crate) stdin: StdioPlan,
444    /// How the command's standard output is wired, when the launch captures
445    /// nothing. A capturing launch supplies its own pipe, and a plan that names
446    /// a destination of its own is refused against one.
447    pub(crate) stdout: StdioPlan,
448    /// How the command's standard error is wired, on the same terms as
449    /// [`stdout`](Self::stdout).
450    pub(crate) stderr: StdioPlan,
451    /// Whether the launch stage starts a new session, so the sandbox has no
452    /// controlling terminal.
453    ///
454    /// Derived from [`stdin`](Self::stdin) alone: it is the stream whose
455    /// inheritance carries the caller's session with it. A command that
456    /// inherits the caller's standard input has been handed the caller's
457    /// terminal along with it and stays in the caller's session; one that does
458    /// not inherit it gets a session of its own, in which `/dev/tty` names
459    /// nothing and `TIOCSTI` is refused. That closes what is reached through
460    /// the session; the output pair is closed by its own disposition. See
461    /// [`Stdio`](crate::Stdio).
462    pub(crate) own_session: bool,
463    /// Whether the supervisor stops the sandbox when the control socket
464    /// hangs up — the caller exited or dropped the last handle.
465    pub(crate) stop_with_caller: bool,
466    /// The hostname to set inside the UTS namespace, when configured.
467    pub(crate) hostname: Option<CString>,
468    /// Whether to bring up loopback in the isolated network namespace.
469    pub(crate) configure_loopback: bool,
470    /// The network posture the sandbox was built with.
471    ///
472    /// Carried rather than reconstructed from [`unshare`](Self::unshare) and
473    /// [`configure_loopback`](Self::configure_loopback), which encode it
474    /// between them: `resolved_inputs` reports it, and a record derived from
475    /// two flags would go quietly wrong the first time a third posture did not
476    /// map onto them. A restriction creates no network namespace, so it carries
477    /// [`Network::Host`](crate::Network::Host) — the caller's, which is what it
478    /// runs in.
479    pub(crate) network: crate::Network,
480    /// The working directory inside the sandbox; `None` means `/`.
481    pub(crate) workdir: Option<CString>,
482    /// The mount profile, lowered into frozen ops the sandbox stage executes
483    /// in order between binding the rootfs and pivoting into it.
484    pub(crate) ops: Vec<MountOp>,
485    /// Caller-side labels parallel to `ops`, naming each mount in a setup
486    /// error. Never touched by the sandbox stages.
487    pub(crate) op_labels: Vec<String>,
488    /// The mount targets the launch has to create as empty files, removed once
489    /// the sandbox that needed them is gone.
490    pub(crate) managed_placeholders: ManagedPlaceholders,
491    /// The caller-side label naming the command in a failed exec step: the
492    /// program path, or the bare name and the candidates a path lookup tried.
493    /// Never touched by the sandbox stages.
494    pub(crate) exec_label: String,
495    /// The resource limits applied to the command process before the
496    /// hardening layer, in a deterministic order.
497    pub(crate) rlimits: Vec<RlimitPlan>,
498    /// Caller-side labels parallel to `rlimits`, naming each limit in a setup
499    /// error. Never touched by the sandbox stages.
500    pub(crate) rlimit_labels: Vec<String>,
501    /// The frozen hardening plan applied in the command stage before exec.
502    /// Present only with the `hardening` feature.
503    #[cfg(feature = "hardening")]
504    pub(crate) hardening: HardeningPlan,
505}
506
507/// A single BPF instruction, the unit of a compiled seccomp program.
508///
509/// Layout-compatible with the kernel's `struct sock_filter`. A caller
510/// building a program with an external compiler passes a `Vec<SockFilter>`
511/// through [`SeccompPolicy::Program`](crate::SeccompPolicy::Program).
512#[cfg(feature = "hardening")]
513#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
514#[repr(C)]
515pub struct SockFilter {
516    /// The instruction opcode.
517    pub code: u16,
518    /// Jump offset when the test is true.
519    pub jt: u8,
520    /// Jump offset when the test is false.
521    pub jf: u8,
522    /// The generic argument.
523    pub k: u32,
524}
525
526// The kernel reads a program as an array of these, so a layout that drifts
527// from `struct sock_filter` must fail the build rather than misissue every
528// instruction of every filter.
529#[cfg(feature = "hardening")]
530const _: () = assert!(size_of::<SockFilter>() == 8);
531
532/// One frozen Landlock grant: an absolute sandbox path and the kernel
533/// `LANDLOCK_ACCESS_FS_*` bitmask allowed beneath it. The command stage masks
534/// the access to the running kernel's ABI and opens the path after the pivot.
535#[cfg(feature = "hardening")]
536#[derive(Debug, Clone)]
537pub(crate) struct LandlockRule {
538    /// The grant's target, an absolute path resolved inside the pivoted root.
539    pub(crate) path: CString,
540    /// The unmasked `LANDLOCK_ACCESS_FS_*` rights allowed beneath the path.
541    pub(crate) access: u64,
542}
543
544/// One frozen Landlock network grant: a TCP port and the kernel
545/// `LANDLOCK_ACCESS_NET_*` bitmask allowed on it. The command stage masks the
546/// access to the running kernel's ABI. The port is host-order and fits a
547/// `u16`, so the kernel's rejection of ports above 65535 cannot arise.
548#[cfg(feature = "hardening")]
549#[derive(Debug, Clone)]
550pub(crate) struct LandlockNetRule {
551    /// The TCP port the grant governs.
552    pub(crate) port: u16,
553    /// The unmasked `LANDLOCK_ACCESS_NET_*` rights allowed on the port.
554    pub(crate) access: u64,
555}
556
557/// The frozen hardening plan: Landlock rules, a compiled seccomp program, and
558/// the capability set to retain, all applied in the command stage between the
559/// stdio wiring and `execve`. Every field is empty in a sandbox that requests
560/// no hardening.
561#[cfg(feature = "hardening")]
562#[derive(Debug, Default, Clone)]
563pub(crate) struct HardeningPlan {
564    /// Landlock filesystem grants; an empty list means the ruleset governs no
565    /// filesystem access.
566    pub(crate) landlock: Vec<LandlockRule>,
567    /// The `handled_access_fs` mask the ruleset governs — the full modeled
568    /// set, masked to the kernel ABI in the child. Zero when no filesystem
569    /// grant applies.
570    pub(crate) landlock_handled: u64,
571    /// Landlock network grants; an empty list means the ruleset governs no
572    /// network access. A ruleset is built when either grant list is non-empty.
573    pub(crate) landlock_net: Vec<LandlockNetRule>,
574    /// The `handled_access_net` mask the ruleset governs — the full modeled
575    /// set, masked to the kernel ABI in the child. Zero when no network grant
576    /// applies.
577    pub(crate) landlock_net_handled: u64,
578    /// The compiled seccomp program, installed verbatim when present.
579    pub(crate) seccomp: Option<Vec<SockFilter>>,
580    /// The capability bits to retain across the drop; `None` leaves the
581    /// namespaced capability set untouched.
582    pub(crate) keep_caps: Option<u64>,
583    /// Whether to lock `SECBIT_NO_SETUID_FIXUP` before the identity switch,
584    /// so a kept capability set survives it. Set only when a non-root
585    /// run-as identity is combined with a capability keep set.
586    pub(crate) set_securebits: bool,
587}
588
589#[cfg(feature = "hardening")]
590impl HardeningPlan {
591    /// Whether the command stage has any hardening to apply.
592    ///
593    /// `set_securebits` is included for completeness even though it never holds
594    /// alone: it is set only alongside a capability keep set, so `keep_caps`
595    /// already forces a non-empty plan. Checking it keeps the invariant
596    /// enforced by the code rather than by that coupling.
597    pub(crate) fn is_empty(&self) -> bool {
598        self.landlock.is_empty()
599            && self.landlock_net.is_empty()
600            && self.seccomp.is_none()
601            && self.keep_caps.is_none()
602            && !self.set_securebits
603    }
604
605    /// Checks that the plan's Landlock grants can be enforced on this host.
606    ///
607    /// For the restriction fallback, whose only filesystem and network boundary
608    /// is Landlock: when the plan requests a grant but this host's Landlock ABI
609    /// (or the absence of the LSM) leaves that grant governing nothing — every
610    /// right of it masked away, as a network grant is on a kernel below ABI 4 —
611    /// the command would run unconfined for that access kind. Refuse at build
612    /// time rather than silently drop the confinement.
613    ///
614    /// The check is per access kind: a filesystem grant that survives the ABI
615    /// must not paper over a network grant the same ABI masks away, or a
616    /// restriction configured to "apply these filesystem grants and connect only
617    /// to port 443" would enforce the filesystem side while leaving the network
618    /// unrestricted. A plan with no Landlock grant passes; a cage does not call
619    /// this — for its filesystem grants and for an isolated-network cage the
620    /// namespace isolation remains the boundary — but see
621    /// [`ensure_host_net_enforceable`](Self::ensure_host_net_enforceable) for
622    /// the one case a cage must check.
623    pub(crate) fn ensure_landlock_enforceable(&self) -> Result<(), crate::error::ConfigError> {
624        if self.landlock.is_empty() && self.landlock_net.is_empty() {
625            return Ok(());
626        }
627        let abi = probe_landlock_abi();
628        let unenforceable = match abi {
629            None => true,
630            Some(abi) => {
631                crate::hardening::fs_grant_masked_away(self.landlock_handled, abi)
632                    || crate::hardening::net_grant_masked_away(self.landlock_net_handled, abi)
633            }
634        };
635        if unenforceable {
636            return Err(crate::error::ConfigError::RestrictionLandlockUnenforceable { abi });
637        }
638        Ok(())
639    }
640
641    /// Checks that a host-network cage's Landlock network grant can be enforced.
642    ///
643    /// Under [`Network::Host`](crate::Network::Host) there is no network
644    /// namespace, so a Landlock network grant is the sandbox's only network
645    /// boundary — exactly the restriction fallback's situation for that one
646    /// access kind. When the grant is masked away (a kernel below Landlock ABI
647    /// 4, or the LSM absent), the command would reach the host network
648    /// unrestricted despite the explicit grant; refuse the build instead.
649    ///
650    /// A cage with no network grant, or one whose network is
651    /// [`Isolated`](crate::Network::Isolated) or [`None`](crate::Network::None),
652    /// does not need this: the network namespace is the boundary there, and a
653    /// masked-away Landlock grant is a best-effort downgrade of additive
654    /// hardening. The caller invokes this only on the host-network path.
655    pub(crate) fn ensure_host_net_enforceable(&self) -> Result<(), crate::error::ConfigError> {
656        if self.landlock_net.is_empty() {
657            return Ok(());
658        }
659        let abi = probe_landlock_abi();
660        let unenforceable = match abi {
661            None => true,
662            Some(abi) => crate::hardening::net_grant_masked_away(self.landlock_net_handled, abi),
663        };
664        if unenforceable {
665            return Err(crate::error::ConfigError::HostNetworkLandlockUnenforceable { abi });
666        }
667        Ok(())
668    }
669}
670
671/// One frozen mount operation.
672///
673/// Every target path is a rootfs-relative `CString` the sandbox stage
674/// resolves against the new-root descriptor with `RESOLVE_IN_ROOT`, so
675/// rootfs-internal absolute symlinks cannot escape into the host during the
676/// pre-pivot window.
677#[derive(Debug, Clone)]
678pub(crate) struct MountOp {
679    /// Mount-target directories to create first, outermost first. Existing
680    /// entries are left alone.
681    pub(crate) dirs: Vec<MountDir>,
682    /// A rootfs-relative file to create (empty, mode 0644) as the target of
683    /// a file bind. An entry the root already ships at that path is left
684    /// exactly as it is, whatever kind it is.
685    ///
686    /// Unlike a mount point directory, it does not outlive the sandbox: see
687    /// [`LaunchPlan::managed_placeholders`], which the caller side removes.
688    pub(crate) create_file: Option<CString>,
689    /// The mount action itself.
690    pub(crate) action: MountAction,
691}
692
693/// One mount-target directory to create, and the mode to create it at.
694///
695/// A mount point the sandbox creates outlives the sandbox: it persists in the
696/// rootfs directory on the host, and a provisioned tree carries it into
697/// whatever is made of the tree afterwards. Its mode is therefore part of what
698/// a launch contributes, and is stated here rather than left to the mount that
699/// covers it — which hides the mode for as long as it is mounted.
700#[derive(Debug, Clone)]
701pub(crate) struct MountDir {
702    /// The rootfs-relative directory to create it in; the empty string is the
703    /// rootfs itself.
704    pub(crate) parent: CString,
705    /// The directory's own name.
706    pub(crate) leaf: CString,
707    /// The mode to create it at, applied exactly.
708    pub(crate) mode: RawMode,
709}
710
711/// The action a [`MountOp`] performs once its target exists.
712#[derive(Debug, Clone)]
713pub(crate) enum MountAction {
714    /// Mount a tmpfs.
715    Tmpfs {
716        /// Rootfs-relative mount target.
717        target: CString,
718        /// Mount flags for the tmpfs.
719        flags: MountFlags,
720        /// Filesystem data string (`mode=...`).
721        data: CString,
722    },
723    /// Mount a fresh procfs instance, presenting the sandbox's own PID
724    /// namespace.
725    Procfs {
726        /// Rootfs-relative mount target.
727        target: CString,
728    },
729    /// Mount a fresh devpts instance.
730    Devpts {
731        /// Rootfs-relative mount target.
732        target: CString,
733        /// Filesystem data string.
734        data: CString,
735    },
736    /// Recursively bind a host path onto a rootfs target.
737    Bind {
738        /// Absolute host source path, canonicalized at build time and
739        /// resolved by the sandbox stage before the pivot, while the host
740        /// view is still mounted.
741        source: CString,
742        /// Rootfs-relative mount target.
743        target: CString,
744        /// Whether to remount the bind read-only.
745        read_only: bool,
746    },
747    /// Perform a raw mount: everything but the target goes to the kernel
748    /// verbatim.
749    Raw {
750        /// The mount source, as given; empty when none was.
751        source: CString,
752        /// Rootfs-relative mount target.
753        target: CString,
754        /// The filesystem type, as given; empty when none was.
755        fstype: CString,
756        /// The raw mount flags, as given.
757        flags: MountFlags,
758        /// The filesystem data string, as given; empty when none was.
759        data: CString,
760    },
761    /// Create a symlink inside the rootfs.
762    Symlink {
763        /// Rootfs-relative directory holding the link.
764        parent: CString,
765        /// The link's name inside `parent`.
766        leaf: CString,
767        /// The link's content.
768        content: CString,
769    },
770}
771
772impl MountAction {
773    /// The rootfs-relative path this action mounts something on, or `None` for
774    /// the one action that mounts nothing.
775    ///
776    /// A path under one of these is inside that mount once the action has run,
777    /// not in the rootfs directory on the host, which is what decides whether
778    /// a target the launch creates has a host path at all.
779    pub(crate) fn target(&self) -> Option<&CStr> {
780        match self {
781            MountAction::Tmpfs { target, .. }
782            | MountAction::Procfs { target }
783            | MountAction::Devpts { target, .. }
784            | MountAction::Bind { target, .. }
785            | MountAction::Raw { target, .. } => Some(target),
786            MountAction::Symlink { .. } => None,
787        }
788    }
789}
790
791/// The wire encoding of the status record: the command exited with a code.
792pub(crate) const STATUS_EXITED: u32 = 1;
793/// The wire encoding of the status record: the command was terminated by a
794/// signal.
795pub(crate) const STATUS_SIGNALED: u32 = 2;
796
797/// Decodes the 8-byte status record the supervisor writes.
798///
799/// `None` for a kind this build never writes, which is a defect in this
800/// library: the reader reports it as a broken protocol rather than panicking,
801/// the same posture the report decoder takes.
802pub(crate) fn decode_status(record: [u8; 8]) -> Option<ExitStatus> {
803    let kind = u32::from_le_bytes(record[0..4].try_into().expect("slice length is 4"));
804    let value = i32::from_le_bytes(record[4..8].try_into().expect("slice length is 4"));
805    match kind {
806        STATUS_EXITED => Some(ExitStatus::exited(value)),
807        STATUS_SIGNALED => Some(ExitStatus::signaled(value)),
808        _ => None,
809    }
810}
811
812/// A launched sandbox, as handed to the running-sandbox handle: the command
813/// is executing and these are the caller's ends of its channels.
814pub(crate) struct Launched {
815    /// Pidfd of the supervisor: the sandbox init inside the PID namespace,
816    /// or the command process itself without one. The kill handle.
817    pub(crate) pidfd: OwnedFd,
818    /// The supervisor's host pid, informational.
819    pub(crate) pid: u32,
820    /// Caller end of the control socket.
821    pub(crate) control: OwnedFd,
822    /// Read end of the status pipe.
823    pub(crate) status: OwnedFd,
824    /// Read end of the command's captured standard output, when observing.
825    pub(crate) stdout: Option<OwnedFd>,
826    /// Read end of the command's captured standard error, when observing.
827    pub(crate) stderr: Option<OwnedFd>,
828    /// A pidfd of the launch stage, for the handle to reap after the status
829    /// arrives. `None` with a PID namespace, where the launch stage is reaped
830    /// at spawn time and the init is not the caller's child process. A pidfd
831    /// rather than a raw pid so the deferred reap targets exactly this process
832    /// and cannot alias a pid a concurrent reaper freed and the OS recycled.
833    pub(crate) reap: Option<OwnedFd>,
834}
835
836/// The caller's ends of a freshly forked launch: the launch stage's pid and
837/// every channel the caller keeps, returned by [`fork_launch`] in the parent.
838struct ParentSide {
839    /// The launch stage (stage A), forked directly from the caller.
840    stage_a: Pid,
841    /// Caller end of the control socket.
842    host_sock: OwnedFd,
843    /// Read end of the report pipe.
844    report_read: OwnedFd,
845    /// Read end of the status pipe.
846    status_read: OwnedFd,
847    /// Read end of the captured standard output, when observing.
848    stdout: Option<OwnedFd>,
849    /// Read end of the captured standard error, when observing.
850    stderr: Option<OwnedFd>,
851    /// Write end of the proceed pipe, present only for a gated launch: a
852    /// write releases the gated command, a close abandons it.
853    proceed_write: Option<OwnedFd>,
854    /// Caller end of the map gate, present only for a range-mapped launch:
855    /// stage A signals ready on it after unsharing, the caller applies the
856    /// map against stage A's pid, and the go byte releases the stage.
857    map_gate: Option<OwnedFd>,
858    /// The primary of the pseudoterminal allocated for a terminal launch. The
859    /// caller's end, and the only descriptor to this terminal that stays on
860    /// this side.
861    primary: Option<OwnedFd>,
862}
863
864/// The go byte written to the proceed pipe to release a gated command. Its
865/// value is immaterial; the child treats any byte as the signal to proceed.
866const PROCEED_BYTE: u8 = b'G';
867
868/// How a forked helper child's identity map is established.
869///
870/// Shared by the export child, which reads a tree as root of the mapped
871/// namespace, and the copy-in child, which writes one: both need to be inside
872/// the map the tree's ownership is expressed in, and neither can establish a
873/// range map on its own.
874pub(crate) enum ChildMap<'a> {
875    /// The single-identity map, which the child writes itself under the
876    /// kernel's unprivileged one-id exception. The lines are formatted by the
877    /// caller before the fork, as a launch formats its own.
878    SelfMap {
879        /// The `uid_map` line: `0 <euid> 1\n`.
880        uid_line: &'a [u8],
881        /// The `gid_map` line: `0 <egid> 1\n`.
882        gid_line: &'a [u8],
883    },
884    /// A range map, written from outside the new namespace by the resolved
885    /// delegate while the child blocks at the map gate.
886    Delegated {
887        /// The delegate that applies the map.
888        mapper: &'a dyn IdMapper,
889        /// The extents, resolved by the caller.
890        map: &'a ResolvedMap,
891    },
892}
893
894impl ChildMap<'_> {
895    /// The child's half of this map: what the forked process itself must do.
896    ///
897    /// The delegate a range map is applied by runs in the caller, so it does
898    /// not cross the fork.
899    pub(in crate::mechanism) fn duty(&self) -> child::MapDuty<'_> {
900        match self {
901            // A provisioning child runs on the caller's own mount view, so its
902            // map files are under the host's `/proc`.
903            ChildMap::SelfMap { uid_line, gid_line } => child::MapDuty::Write {
904                paths: child::MapPaths::SELF,
905                uid_line,
906                gid_line,
907            },
908            ChildMap::Delegated { .. } => child::MapDuty::Await,
909        }
910    }
911}
912
913/// The ready byte a stage sends on its map gate once it has unshared and is
914/// blocked waiting for the identity map. Every child that waits for a
915/// delegated map sends it: stage A, the command stage at its nested entry, the
916/// export child, the copy-in child, and the removal child.
917pub(super) const MAP_READY_BYTE: u8 = b'R';
918
919/// The go byte the delegate sends on the map gate once the identity map is
920/// established — the caller for a launch's own namespace, and the process that
921/// forked the command for its nested one. The waiting side treats any byte as
922/// the signal.
923pub(super) const MAP_GO_BYTE: u8 = b'M';
924
925/// The length of a helper child's failure record: a step's wire value then an
926/// errno, both little-endian.
927pub(super) const RECORD_LEN: usize = 8;
928
929/// How driving a map gate ended.
930pub(super) enum Gated {
931    /// The map is in force and the child has been released.
932    Released,
933    /// The child reported a failure before it signalled ready, so it never
934    /// reached the gate. The record is the caller's to decode, since only the
935    /// caller knows which step vocabulary it names.
936    EarlyRecord([u8; RECORD_LEN]),
937    /// The child is gone, or said something that is neither a ready byte nor a
938    /// failure record. Nothing was applied and nothing was released.
939    ChildLost,
940}
941
942/// Why driving a map gate failed.
943pub(super) enum GateFailure {
944    /// A read or write on the gate itself failed.
945    Io(Errno),
946    /// The delegate refused to establish the map; its own error is the
947    /// diagnostic.
948    Map(crate::idmap::IdMapError),
949}
950
951/// The caller's half of a map gate: waits for the child's ready byte, has the
952/// delegate establish the child's identity map, and releases the child.
953///
954/// Every provisioning child that takes a delegated map is driven through this:
955/// the export, the copy-in, and the removal. The child's side of the same
956/// handshake is [`child::enter_mapped_namespace`], which is why the two byte
957/// constants above are shared rather than restated.
958///
959/// The gate is a seqpacket socket, so one read receives one whole message and
960/// its length distinguishes the two things a child may send: one byte is
961/// [`MAP_READY_BYTE`], and [`RECORD_LEN`] bytes is a failure the child hit
962/// before it ever reached the gate — it could not sweep its descriptors, or
963/// could not unshare. Reading the record here is what lets that failure name
964/// its step instead of surfacing as a child that vanished.
965///
966/// Nothing is killed or reaped here. A caller that gets anything but
967/// [`Gated::Released`] has a child blocked in an unmapped namespace, and
968/// [`abandon_gated_child`] is how it ends it.
969///
970/// The fork that produces the child stays in each helper's own module even
971/// though the three read alike. The child branch runs a different
972/// never-returning function each time, and the `unsafe` block's SAFETY comment
973/// has to vouch for *that* function's post-fork discipline by name; a shared
974/// harness would leave the comment vouching for a closure whose body is
975/// somewhere else.
976pub(super) fn drive_map_gate(
977    gate: BorrowedFd<'_>,
978    child: Pid,
979    mapper: &dyn crate::idmap::IdMapper,
980    map: &crate::idmap::ResolvedMap,
981) -> Result<Gated, GateFailure> {
982    let mut record = [0u8; RECORD_LEN];
983    let received =
984        frame::retry_on_intr!(rustix::io::read(gate, &mut record)).map_err(GateFailure::Io)?;
985    match received {
986        1 if record[0] == MAP_READY_BYTE => {}
987        RECORD_LEN => return Ok(Gated::EarlyRecord(record)),
988        _ => return Ok(Gated::ChildLost),
989    }
990
991    let pid = child.as_raw_nonzero().get() as u32;
992    mapper.apply(pid, map).map_err(GateFailure::Map)?;
993
994    // A failed write means the child is already gone. Whatever it was going to
995    // report, it reports through its own channel or not at all; that is not a
996    // failure of the gate.
997    let _ = frame::retry_on_intr!(rustix::io::write(gate, &[MAP_GO_BYTE]));
998    Ok(Gated::Released)
999}
1000
1001/// Ends a child left blocked at its map gate, leaving it to be reaped.
1002///
1003/// A child whose map the delegate could not establish is waiting for a go byte
1004/// that will never come. It holds nothing but its own descriptors and has
1005/// touched no tree, so killing it is a clean end — and it is the only end
1006/// available.
1007///
1008/// Killed rather than released by closing the gate. `kernel_fork` copies the
1009/// whole descriptor table, so any process this caller forked concurrently holds
1010/// its own copy of this gate's caller end and keeps the child's peer open
1011/// however promptly this one closes. The library's own children sweep their
1012/// inherited descriptors, but each only once its own fork has returned in it,
1013/// and a child the *consumer* forked never sweeps at all. The end of input the
1014/// blocked child is waiting for is therefore not something this caller can make
1015/// arrive, and a reap would block on it forever.
1016///
1017/// The reap is separate because it is the caller's, and exactly one caller may
1018/// perform it: a pid is the kernel's to reuse the moment its status is
1019/// collected, so a second wait on the same number can collect an unrelated
1020/// child of this process instead. A caller whose own path already waits calls
1021/// this; one that returns without waiting calls
1022/// [`abandon_gated_child`] and has the reap done here.
1023pub(super) fn kill_gated_child(child: Pid) {
1024    let _ = process::kill_process(child, Signal::KILL);
1025}
1026
1027/// [`kill_gated_child`], and reaps it.
1028///
1029/// For a caller that gives up on a child by returning, so nothing further in
1030/// its own path would collect the status.
1031pub(super) fn abandon_gated_child(child: Pid) {
1032    kill_gated_child(child);
1033    let _ = wait_for_exit(child);
1034}
1035
1036/// Marshals the launch data, creates the launch channels, and forks the
1037/// launch stage.
1038///
1039/// In the child this never returns: it constructs the [`child::Context`] and
1040/// runs [`child::stage_a`], which ends in `execve` or `exit_group`. In the
1041/// parent it returns the caller's ends of the channels. A `gated` launch
1042/// additionally carries a proceed pipe, whose read end the command stage
1043/// blocks on before `execve`; a `terminal` launch additionally allocates a
1044/// pseudoterminal, whose replica the sandbox wires onto all three standard
1045/// streams and whose primary comes back to the caller.
1046fn fork_launch(
1047    plan: &LaunchPlan,
1048    observe: bool,
1049    gated: bool,
1050    terminal: Option<&Terminal>,
1051) -> Result<ParentSide, Error> {
1052    // Pre-fork marshaling, caller side. The pointer arrays and identity-map
1053    // lines are the last pieces the sandbox stages need; they are assembled
1054    // here, where allocation is still permitted, and only read after the
1055    // fork.
1056    let mut argv: Vec<*const u8> = Vec::with_capacity(plan.args.len() + 2);
1057    argv.push(plan.program.as_ptr().cast());
1058    argv.extend(plan.args.iter().map(|arg| arg.as_ptr().cast::<u8>()));
1059    argv.push(std::ptr::null());
1060    let mut envp: Vec<*const u8> = Vec::with_capacity(plan.env.len() + 1);
1061    envp.extend(plan.env.iter().map(|entry| entry.as_ptr().cast::<u8>()));
1062    envp.push(std::ptr::null());
1063
1064    // The single-identity map lines: root inside is the calling user
1065    // outside. The kernel requires the mapped outer id to be the writer's
1066    // effective id, so the lines are formatted from the current euid/egid —
1067    // captured here, before any fork or unshare, where geteuid still
1068    // answers with the caller's id. A range-mapped launch writes no lines;
1069    // its map goes through the delegate against the gated stage.
1070    let (uid_line, gid_line) = match &plan.identity {
1071        IdentityPlan::Single => (
1072            format!("0 {} 1\n", process::geteuid().as_raw()).into_bytes(),
1073            format!("0 {} 1\n", process::getegid().as_raw()).into_bytes(),
1074        ),
1075        IdentityPlan::Ranged { .. } => (Vec::new(), Vec::new()),
1076    };
1077
1078    let (report_read, report_write) = pipe::pipe_with(PipeFlags::CLOEXEC).map_err(spawn_error)?;
1079    let (status_read, status_write) = pipe::pipe_with(PipeFlags::CLOEXEC).map_err(spawn_error)?;
1080    // Seqpacket keeps the pidfd delivery and each control request a single
1081    // atomic message.
1082    let (host_sock, cage_sock) = rustix::net::socketpair(
1083        AddressFamily::UNIX,
1084        SocketType::SEQPACKET,
1085        SocketFlags::CLOEXEC,
1086        None,
1087    )
1088    .map_err(spawn_error)?;
1089    // The map gate, created only when a delegate writes the map: one
1090    // seqpacket pair carries ready out and go back, the shape the control
1091    // socket already set.
1092    let map_gate = if plan.identity.is_ranged() {
1093        Some(
1094            rustix::net::socketpair(
1095                AddressFamily::UNIX,
1096                SocketType::SEQPACKET,
1097                SocketFlags::CLOEXEC,
1098                None,
1099            )
1100            .map_err(spawn_error)?,
1101        )
1102    } else {
1103        None
1104    };
1105    // One descriptor serves the null stdin disposition and the supervisor's
1106    // own stdio; opened read-write so it fits both.
1107    let null = rustix::fs::open(
1108        c"/dev/null",
1109        rustix::fs::OFlags::RDWR | rustix::fs::OFlags::CLOEXEC,
1110        rustix::fs::Mode::empty(),
1111    )
1112    .map_err(spawn_error)?;
1113    let (stdout_pipe, stderr_pipe) = if observe {
1114        (
1115            Some(pipe::pipe_with(PipeFlags::CLOEXEC).map_err(spawn_error)?),
1116            Some(pipe::pipe_with(PipeFlags::CLOEXEC).map_err(spawn_error)?),
1117        )
1118    } else {
1119        (None, None)
1120    };
1121    // The proceed pipe exists only for a gated launch.
1122    let proceed = if gated {
1123        Some(pipe::pipe_with(PipeFlags::CLOEXEC).map_err(spawn_error)?)
1124    } else {
1125        None
1126    };
1127    // The pseudoterminal, last of the launch's own descriptors. Allocated after
1128    // them so its replica cannot land on a standard-stream number: the six
1129    // channel descriptors above are already taken, whatever the caller left open.
1130    let terminal = terminal.map(crate::terminal::allocate).transpose()?;
1131
1132    // SAFETY: the child-process branch only constructs a struct of references
1133    // on its own stack and runs `child::stage_a`, which performs raw rustix
1134    // syscalls over the data frozen above — no allocation, no locking, no
1135    // libc — and never returns: every path ends in `execve` or `exit_group`.
1136    match unsafe { runtime::kernel_fork() } {
1137        Err(errno) => Err(spawn_error(errno)),
1138        Ok(Fork::Child(_)) => {
1139            let context = child::Context {
1140                plan,
1141                argv: &argv,
1142                envp: &envp,
1143                uid_line: &uid_line,
1144                gid_line: &gid_line,
1145                report_fd: report_write.as_fd(),
1146                status_fd: status_write.as_fd(),
1147                control_fd: cage_sock.as_fd(),
1148                null_fd: null.as_fd(),
1149                capture_stdout_fd: stdout_pipe.as_ref().map(|(_, write)| write.as_fd()),
1150                capture_stderr_fd: stderr_pipe.as_ref().map(|(_, write)| write.as_fd()),
1151                proceed_fd: proceed.as_ref().map(|(read, _)| read.as_fd()),
1152                map_gate_fd: map_gate.as_ref().map(|(_, stage)| stage.as_fd()),
1153                stdin_fd: plan.stdin.caller_fd(),
1154                stdout_fd: plan.stdout.caller_fd(),
1155                stderr_fd: plan.stderr.caller_fd(),
1156                terminal_fd: terminal.as_ref().map(|(_, replica)| replica.as_fd()),
1157            };
1158            child::stage_a(&context)
1159        }
1160        Ok(Fork::ParentOf(stage_a)) => {
1161            // Close the caller's copies of the sandbox-side ends: from here
1162            // on, end-of-file on each read side means the sandbox's copies
1163            // are gone.
1164            drop(report_write);
1165            drop(status_write);
1166            drop(cage_sock);
1167            drop(null);
1168            let stdout = stdout_pipe.map(|(read, write)| {
1169                drop(write);
1170                read
1171            });
1172            let stderr = stderr_pipe.map(|(read, write)| {
1173                drop(write);
1174                read
1175            });
1176            let proceed_write = proceed.map(|(read, write)| {
1177                drop(read);
1178                write
1179            });
1180            let map_gate = map_gate.map(|(caller, stage)| {
1181                drop(stage);
1182                caller
1183            });
1184            // The replica goes as soon as the fork returns. End-of-file on the
1185            // primary arrives only when the last replica descriptor anywhere
1186            // closes, so every process that is not the command drops its own
1187            // copy — this one, `stage_init`'s, and the supervisor's.
1188            //
1189            // The one copy no launch controls is a sibling's: `kernel_fork`
1190            // copies the whole descriptor table, so a child this caller forked
1191            // between the replica's open just above and this close inherits one,
1192            // exactly as the map gate records. Every child the library forks —
1193            // another launch's stage A, an export, a copy-in, a removal, the
1194            // network stack's tap helper — sweeps its inherited table as its
1195            // first act, which bounds the exposure to that sweep rather than to
1196            // the sibling's life, and the replica is opened close-on-exec so no
1197            // exec'd descendant can hold one at all. A child the *consumer*
1198            // forked in the same window sweeps nothing, and holds its copy until
1199            // it execs or exits; that is the caller's window to keep clear.
1200            let primary = terminal.map(|(primary, replica)| {
1201                drop(replica);
1202                primary
1203            });
1204            Ok(ParentSide {
1205                stage_a,
1206                host_sock,
1207                report_read,
1208                status_read,
1209                stdout,
1210                stderr,
1211                proceed_write,
1212                map_gate,
1213                primary,
1214            })
1215        }
1216    }
1217}
1218
1219/// Establishes a range-mapped launch's identity map through its delegate.
1220///
1221/// A no-op for the single-identity tier, which carries no gate. For a range
1222/// tier: waits for stage A's ready byte (sent once it has unshared and
1223/// swept), applies the resolved map against stage A's pid, and releases the
1224/// stage with the go byte.
1225///
1226/// If stage A dies before signalling ready, the report pipe holds the true
1227/// cause and is read for it. If the delegate fails, the gate is closed —
1228/// stage A sees end-of-file and exits, reporting a gate failure it knows the
1229/// caller will not read — and the delegate's own error is returned: the
1230/// delegate ran caller-side, so the better diagnostic is already in hand.
1231fn establish_identity_map(plan: &LaunchPlan, parent: &mut ParentSide) -> Result<(), Error> {
1232    let Some(gate) = &parent.map_gate else {
1233        return Ok(());
1234    };
1235    let IdentityPlan::Ranged { mapper, map } = &plan.identity else {
1236        // fork_launch creates the gate only for a ranged plan.
1237        return Ok(());
1238    };
1239
1240    let mut byte = [0u8; 1];
1241    loop {
1242        match rustix::io::read(gate, &mut byte) {
1243            // Stage A is unshared and waiting; the namespace exists.
1244            Ok(n) if n > 0 => break,
1245            // End-of-file: stage A died before reaching the gate. The
1246            // report pipe holds the step that failed.
1247            Ok(_) => {
1248                let report = read_report(&parent.report_read);
1249                let _ = wait_for_exit(parent.stage_a);
1250                return Err(match report {
1251                    Ok(Report::SetupFailed {
1252                        step,
1253                        errno,
1254                        detail,
1255                    }) => setup_error(step, errno, detail, Labels::of(plan)),
1256                    Ok(Report::ExecSucceeded) => Error::SupervisorLost,
1257                    Err(err) => err,
1258                });
1259            }
1260            Err(Errno::INTR) => continue,
1261            Err(errno) => {
1262                let _ = process::kill_process(parent.stage_a, Signal::KILL);
1263                let _ = wait_for_exit(parent.stage_a);
1264                return Err(wait_syscall_error(errno));
1265            }
1266        }
1267    }
1268
1269    // The map is written against stage A: the process that unshared, so the
1270    // process whose /proc map files name the new namespace.
1271    let pid = parent.stage_a.as_raw_nonzero().get() as u32;
1272    match mapper.apply(pid, map) {
1273        Ok(()) => {
1274            // Release the stage. A failed write means it died; the caller's
1275            // next read on the report or control path reports the outcome.
1276            let _ = rustix::io::write(gate, &[MAP_GO_BYTE]);
1277            Ok(())
1278        }
1279        Err(err) => {
1280            // The delegate failed. Stage A is blocked at the gate in an
1281            // unmapped namespace, before it forks anything, so killing it ends
1282            // the launch cleanly; its report record is superseded by the
1283            // delegate's error.
1284            //
1285            // Killed rather than released by closing the gate: `kernel_fork`
1286            // copies the whole descriptor table, so any process this caller
1287            // forked concurrently holds a copy of this gate's caller end and
1288            // keeps the peer open however promptly this one closes. The
1289            // library's own children do sweep their inherited tables, but each
1290            // only after its fork has returned in it, and nothing bounds when
1291            // the kernel next schedules that child; a child the *consumer*
1292            // forked sweeps nothing at all, and drops the copy only when it
1293            // execs — the gate is close-on-exec — or exits. The end-of-file
1294            // stage A waits on is therefore not something this caller can make
1295            // arrive, and the reap below would block on it.
1296            let _ = process::kill_process(parent.stage_a, Signal::KILL);
1297            parent.map_gate = None;
1298            let _ = wait_for_exit(parent.stage_a);
1299            Err(Error::IdentityMap(err))
1300        }
1301    }
1302}
1303
1304/// Decides how the launch stage is reaped for a command that is now running.
1305///
1306/// Without a PID namespace the launch stage stays outside as the command's
1307/// parent and is reaped by the handle once the status arrives, so it is
1308/// returned; with one it has already exited and is reaped here, best-effort in
1309/// case the application reaps child processes of its own.
1310fn reap_after(pid_namespace: bool, stage_a: Pid) -> Option<OwnedFd> {
1311    if pid_namespace {
1312        // The launch stage is short-lived here and reaped now, before its pid
1313        // could be reused: it is still an un-reaped child at this point.
1314        let _ = wait_for_exit(stage_a);
1315        None
1316    } else {
1317        // The launch stage outlives spawn here, so the handle reaps it later.
1318        // Take a pidfd now, while it is still a child, so that deferred reap
1319        // targets exactly this process rather than a raw pid a concurrent
1320        // reaper could free and the OS recycle. If it was already reaped,
1321        // `pidfd_open` fails and there is nothing left to reap.
1322        process::pidfd_open(stage_a, PidfdFlags::empty()).ok()
1323    }
1324}
1325
1326/// Reaps the process a pidfd refers to, blocking until it exits.
1327///
1328/// Immune to pid reuse: the pidfd names the exact process, so unlike a raw
1329/// `waitpid` this cannot alias a pid a concurrent reaper freed and the OS
1330/// recycled for another same-parent child.
1331pub(crate) fn reap_via_pidfd(pidfd: BorrowedFd<'_>) -> Result<(), Errno> {
1332    frame::retry_on_intr!(process::waitid(WaitId::PidFd(pidfd), WaitIdOptions::EXITED)).map(|_| ())
1333}
1334
1335/// Launches the sandbox and blocks until the command is executing.
1336///
1337/// Returns once the report pipe settles: end-of-file means every setup step
1338/// succeeded and `execve` replaced the command process's image, and a record
1339/// means a step failed and is returned as the typed error. A terminal launch
1340/// returns the caller's end of the pseudoterminal beside the launched sandbox;
1341/// every other launch returns `None` for it.
1342pub(crate) fn spawn(
1343    plan: &LaunchPlan,
1344    observe: bool,
1345    terminal: Option<&Terminal>,
1346    progress: &mut dyn FnMut(Progress),
1347) -> Result<(Launched, Option<OwnedFd>), Error> {
1348    progress(Progress::Launching);
1349    let mut parent = fork_launch(plan, observe, false, terminal)?;
1350    // A range map is established before the supervisor is published, so a
1351    // published sandbox always has a fully mapped user namespace.
1352    establish_identity_map(plan, &mut parent)?;
1353    let received = receive_supervisor(&parent.host_sock);
1354    if received.is_ok() {
1355        progress(Progress::Supervised);
1356    }
1357    let report = read_report(&parent.report_read);
1358
1359    match (report, received) {
1360        // The command is running and the supervisor is published.
1361        (Ok(Report::ExecSucceeded), Ok((pidfd, pid))) => {
1362            progress(Progress::Executing);
1363            Ok((
1364                Launched {
1365                    pidfd,
1366                    pid,
1367                    control: parent.host_sock,
1368                    status: parent.status_read,
1369                    stdout: parent.stdout,
1370                    stderr: parent.stderr,
1371                    reap: reap_after(plan.pid_namespace, parent.stage_a),
1372                },
1373                parent.primary,
1374            ))
1375        }
1376        // A setup step failed. Every stage at or below the failure has
1377        // exited, and a supervisor above it exits as soon as it observes the
1378        // exit, so the launch stage is reapable.
1379        (
1380            Ok(Report::SetupFailed {
1381                step,
1382                errno,
1383                detail,
1384            }),
1385            _,
1386        ) => {
1387            let _ = wait_for_exit(parent.stage_a);
1388            Err(setup_error(step, errno, detail, Labels::of(plan)))
1389        }
1390        // The report pipe reached end-of-file — every setup step succeeded —
1391        // but the supervisor was never published: the launch stage exited
1392        // before sending it.
1393        (Ok(Report::ExecSucceeded), Err(err)) => {
1394            let _ = wait_for_exit(parent.stage_a);
1395            Err(err)
1396        }
1397        // The report pipe could not be read. Unsupervisable; tear the launch
1398        // down rather than leave it running blind.
1399        (Err(err), received) => {
1400            if let Ok((pidfd, _)) = received {
1401                let _ = process::pidfd_send_signal(&pidfd, Signal::KILL);
1402            }
1403            let _ = process::kill_process(parent.stage_a, Signal::KILL);
1404            let _ = wait_for_exit(parent.stage_a);
1405            Err(err)
1406        }
1407    }
1408}
1409
1410/// A launched sandbox held at the gate before `execve`, the seam at which a
1411/// caller attaches a userspace network stack to the sandbox's network
1412/// namespace.
1413///
1414/// The namespaces exist and the supervisor is published, so [`netns_pid`] is a
1415/// valid target for `/proc/<pid>/ns/net`; the command has not yet run.
1416/// [`proceed`] releases it, and dropping the handle without proceeding tears
1417/// the launch down through [`abandon`].
1418///
1419/// [`netns_pid`]: PendingLaunch::netns_pid
1420/// [`proceed`]: PendingLaunch::proceed
1421/// [`abandon`]: PendingLaunch::abandon
1422pub(crate) struct PendingLaunch {
1423    /// The launch stage, reaped at proceed or abandon.
1424    stage_a: Pid,
1425    /// Pidfd of the supervisor: the kill handle, and the process whose
1426    /// network namespace a stack attaches to.
1427    pidfd: OwnedFd,
1428    /// The supervisor's host pid, in every sandbox namespace.
1429    pid: u32,
1430    /// Caller end of the control socket.
1431    host_sock: OwnedFd,
1432    /// Read end of the report pipe, read once the command is released.
1433    report_read: OwnedFd,
1434    /// Read end of the status pipe.
1435    status_read: OwnedFd,
1436    /// Read end of the captured standard output, when observing.
1437    stdout: Option<OwnedFd>,
1438    /// Read end of the captured standard error, when observing.
1439    stderr: Option<OwnedFd>,
1440    /// Write end of the proceed pipe: a write releases the command.
1441    proceed_write: OwnedFd,
1442    /// Whether the sandbox has its own PID namespace, deciding the reap.
1443    pid_namespace: bool,
1444    /// The mount labels, for naming a setup step that fails after release.
1445    op_labels: Vec<String>,
1446    /// The command label, for naming an exec step that fails after release.
1447    exec_label: String,
1448    /// The resource-limit labels, for naming an rlimit step that fails after
1449    /// release.
1450    rlimit_labels: Vec<String>,
1451}
1452
1453/// Launches the sandbox but holds the command at the gate before `execve`,
1454/// returning once the supervisor is published.
1455///
1456/// A setup failure before the supervisor is published (a denied `unshare`, a
1457/// failed fork) surfaces here as the typed error; a failure after it is
1458/// deferred to [`PendingLaunch::proceed`], where the report is read.
1459pub(crate) fn spawn_pending(
1460    plan: &LaunchPlan,
1461    observe: bool,
1462    terminal: Option<&Terminal>,
1463    progress: &mut dyn FnMut(Progress),
1464) -> Result<(PendingLaunch, Option<OwnedFd>), Error> {
1465    progress(Progress::Launching);
1466    let mut parent = fork_launch(plan, observe, true, terminal)?;
1467    // The map precedes the supervisor's publication, so by the time the
1468    // caller holds a Pending, /proc/<pid>/ns/user is fully mapped — the
1469    // ordering the network stack's helper relies on when it enters the
1470    // namespace.
1471    establish_identity_map(plan, &mut parent)?;
1472    let proceed_write = parent
1473        .proceed_write
1474        .expect("a gated launch carries a proceed pipe");
1475
1476    match receive_supervisor(&parent.host_sock) {
1477        Ok((pidfd, pid)) => {
1478            progress(Progress::Supervised);
1479            // The primary comes back here rather than out of `proceed`: the
1480            // pseudoterminal is allocated before the fork and the gate sits
1481            // after the controlling terminal is established, so the caller
1482            // attaches its network stack and proceeds with the primary already
1483            // in hand. Nothing writes to the terminal while the launch is held.
1484            Ok((
1485                PendingLaunch {
1486                    stage_a: parent.stage_a,
1487                    pidfd,
1488                    pid,
1489                    host_sock: parent.host_sock,
1490                    report_read: parent.report_read,
1491                    status_read: parent.status_read,
1492                    stdout: parent.stdout,
1493                    stderr: parent.stderr,
1494                    proceed_write,
1495                    pid_namespace: plan.pid_namespace,
1496                    op_labels: plan.op_labels.clone(),
1497                    exec_label: plan.exec_label.clone(),
1498                    rlimit_labels: plan.rlimit_labels.clone(),
1499                },
1500                parent.primary,
1501            ))
1502        }
1503        // The supervisor was never published: a setup step failed before the
1504        // command reached the gate. The child reported it and exited; read
1505        // the record for the precise cause, then reap the launch stage.
1506        Err(err) => {
1507            let report = read_report(&parent.report_read);
1508            let _ = wait_for_exit(parent.stage_a);
1509            match report {
1510                Ok(Report::SetupFailed {
1511                    step,
1512                    errno,
1513                    detail,
1514                }) => Err(setup_error(step, errno, detail, Labels::of(plan))),
1515                _ => Err(err),
1516            }
1517        }
1518    }
1519}
1520
1521impl PendingLaunch {
1522    /// The supervisor's host pid: a process in the sandbox's network
1523    /// namespace, the target for `/proc/<pid>/ns/net`.
1524    pub(crate) fn netns_pid(&self) -> u32 {
1525        self.pid
1526    }
1527
1528    /// Releases the gated command and blocks until it is executing, returning
1529    /// the launched sandbox or the typed setup error.
1530    pub(crate) fn proceed(self, progress: &mut dyn FnMut(Progress)) -> Result<Launched, Error> {
1531        // Release the command: any byte is the go signal. A failed write means
1532        // the command process is already gone; the report read below reports
1533        // the true outcome.
1534        let _ = rustix::io::write(&self.proceed_write, &[PROCEED_BYTE]);
1535        drop(self.proceed_write);
1536
1537        match read_report(&self.report_read) {
1538            Ok(Report::ExecSucceeded) => {
1539                progress(Progress::Executing);
1540                Ok(Launched {
1541                    pidfd: self.pidfd,
1542                    pid: self.pid,
1543                    control: self.host_sock,
1544                    status: self.status_read,
1545                    stdout: self.stdout,
1546                    stderr: self.stderr,
1547                    reap: reap_after(self.pid_namespace, self.stage_a),
1548                })
1549            }
1550            Ok(Report::SetupFailed {
1551                step,
1552                errno,
1553                detail,
1554            }) => {
1555                let _ = wait_for_exit(self.stage_a);
1556                // Assembled field by field rather than from `&self`: the
1557                // proceed pipe was moved out of `self` above, so `self` as a
1558                // whole can no longer be borrowed — which is what a helper
1559                // taking `&self` would need. `Labels::of` does not apply
1560                // either; it reads a plan, and this side holds only the labels
1561                // the plan was built with.
1562                Err(setup_error(
1563                    step,
1564                    errno,
1565                    detail,
1566                    Labels {
1567                        ops: &self.op_labels,
1568                        exec: &self.exec_label,
1569                        rlimits: &self.rlimit_labels,
1570                    },
1571                ))
1572            }
1573            Err(err) => {
1574                let _ = process::pidfd_send_signal(&self.pidfd, Signal::KILL);
1575                let _ = process::kill_process(self.stage_a, Signal::KILL);
1576                let _ = wait_for_exit(self.stage_a);
1577                Err(err)
1578            }
1579        }
1580    }
1581
1582    /// Tears the pending launch down: kills the supervisor and reaps the
1583    /// launch stage. The gated command dies with its supervisor — with a PID
1584    /// namespace the kernel tears the namespace down, and without one the
1585    /// supervisor pidfd is the command process itself.
1586    pub(crate) fn abandon(self) {
1587        let _ = process::pidfd_send_signal(&self.pidfd, Signal::KILL);
1588        let _ = process::kill_process(self.stage_a, Signal::KILL);
1589        let _ = wait_for_exit(self.stage_a);
1590    }
1591}
1592
1593/// The caller-side labels that name a failed setup step's subject.
1594///
1595/// The report record's detail index selects within whichever list the failed
1596/// step belongs to — the mount ops for a mount step, the resource limits for
1597/// the rlimit step — and the exec step has a single subject rather than a
1598/// list. The sandbox stages never see any of this; it exists so a reported
1599/// step carries the thing it was operating on.
1600#[derive(Clone, Copy)]
1601struct Labels<'a> {
1602    ops: &'a [String],
1603    exec: &'a str,
1604    rlimits: &'a [String],
1605}
1606
1607impl<'a> Labels<'a> {
1608    /// The labels a plan carries.
1609    fn of(plan: &'a LaunchPlan) -> Labels<'a> {
1610        Labels {
1611            ops: &plan.op_labels,
1612            exec: &plan.exec_label,
1613            rlimits: &plan.rlimit_labels,
1614        }
1615    }
1616}
1617
1618/// Resolves a report record's 1-based detail index against a label list; a
1619/// zero index means the step named no subject.
1620fn index_label(detail: u32, labels: &[String]) -> Option<String> {
1621    detail
1622        .checked_sub(1)
1623        .and_then(|index| labels.get(index as usize))
1624        .cloned()
1625}
1626
1627/// Maps a reported setup failure onto the public error, attaching the label
1628/// naming what the step was operating on and identifying the host
1629/// configuration responsible when namespace creation itself was denied.
1630fn setup_error(step: SetupStep, errno: i32, detail: u32, labels: Labels<'_>) -> Error {
1631    // A denied unshare is almost always a host that disables unprivileged
1632    // user namespaces; report it as such, with the blocking configuration
1633    // when the probe can identify it.
1634    if step == SetupStep::Unshare
1635        && (errno == Errno::PERM.raw_os_error() || errno == Errno::ACCESS.raw_os_error())
1636    {
1637        return Error::UsernsUnavailable {
1638            blocker: host::userns_blocker(),
1639            source: std::io::Error::from_raw_os_error(errno),
1640        };
1641    }
1642    // A user namespace is charged against `user.max_user_namespaces` at every
1643    // level up to the initial namespace, and a launch holds two. `ENOSPC` from
1644    // the nested entry is that budget and nothing else — no other setup step
1645    // reports it — so the error names the sysctl rather than leaving a reader
1646    // with "No space left on device" against a step that touched no filesystem.
1647    if step == SetupStep::NestedUnshare && errno == Errno::NOSPC.raw_os_error() {
1648        return Error::NestedUsernsBudgetExhausted;
1649    }
1650    // The exec step carries no detail index — there is one command, not a list
1651    // — so its subject comes from the plan's own label. Every other step with a
1652    // subject indexes the list it belongs to.
1653    let detail = match step {
1654        SetupStep::Exec => Some(labels.exec.to_string()),
1655        SetupStep::SetRlimit => index_label(detail, labels.rlimits),
1656        _ => index_label(detail, labels.ops),
1657    };
1658    Error::Setup {
1659        step,
1660        source: std::io::Error::from_raw_os_error(errno),
1661        detail,
1662    }
1663}
1664
1665/// Receives the supervisor's pidfd and host pid from the launch stage.
1666///
1667/// The stage sends one seqpacket message: the pid as four little-endian
1668/// bytes, with the pidfd attached as `SCM_RIGHTS` ancillary data. A closed
1669/// socket without the message means the stage exited before publishing.
1670fn receive_supervisor(sock: &OwnedFd) -> Result<(OwnedFd, u32), Error> {
1671    let mut payload = [0u8; 4];
1672    let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
1673    let mut ancillary = RecvAncillaryBuffer::new(&mut space);
1674    let received = loop {
1675        match rustix::net::recvmsg(
1676            sock,
1677            &mut [std::io::IoSliceMut::new(&mut payload)],
1678            &mut ancillary,
1679            RecvFlags::CMSG_CLOEXEC,
1680        ) {
1681            Ok(received) => break received,
1682            Err(Errno::INTR) => continue,
1683            Err(errno) => return Err(wait_syscall_error(errno)),
1684        }
1685    };
1686    if received.bytes != payload.len() {
1687        // A short message means the stage died before publishing the
1688        // supervisor. Any descriptor a truncated message still carried is
1689        // closed when `ancillary` drops — its `Drop` drains and closes every
1690        // undrained `SCM_RIGHTS` fd — so this early return leaks nothing.
1691        return Err(Error::SupervisorLost);
1692    }
1693    let mut pidfd = None;
1694    for message in ancillary.drain() {
1695        if let RecvAncillaryMessage::ScmRights(fds) = message {
1696            pidfd = fds.into_iter().next();
1697        }
1698    }
1699    match pidfd {
1700        Some(pidfd) => Ok((pidfd, u32::from_le_bytes(payload))),
1701        None => Err(Error::SupervisorLost),
1702    }
1703}
1704
1705/// The decoded contents of the report pipe.
1706enum Report {
1707    /// The pipe closed empty: `execve` succeeded and the command is
1708    /// running.
1709    ExecSucceeded,
1710    /// A stage reported a failed setup step and exited. `detail` is the
1711    /// 1-based index of the mount op the step belonged to, or zero.
1712    SetupFailed {
1713        step: SetupStep,
1714        errno: i32,
1715        detail: u32,
1716    },
1717}
1718
1719/// Reads the report pipe to EOF and decodes the record, if any.
1720///
1721/// A record is exactly 12 bytes and, being smaller than `PIPE_BUF`, arrives
1722/// atomically or not at all. Any other length is a broken report protocol,
1723/// surfaced as an error so the caller tears the launch down rather than
1724/// trusting a report it cannot decode.
1725fn read_report(fd: &OwnedFd) -> Result<Report, Error> {
1726    // One byte beyond the record size: a longer read proves corruption
1727    // without consuming an unbounded stream.
1728    let mut buf = [0u8; 13];
1729    let mut len = 0;
1730    while len < buf.len() {
1731        match rustix::io::read(fd, &mut buf[len..]) {
1732            Ok(0) => break,
1733            Ok(n) => len += n,
1734            Err(Errno::INTR) => continue,
1735            Err(errno) => return Err(wait_syscall_error(errno)),
1736        }
1737    }
1738    match len {
1739        0 => Ok(Report::ExecSucceeded),
1740        12 => {
1741            let wire = u32::from_le_bytes(buf[0..4].try_into().expect("slice length is 4"));
1742            let errno = i32::from_le_bytes(buf[4..8].try_into().expect("slice length is 4"));
1743            let detail = u32::from_le_bytes(buf[8..12].try_into().expect("slice length is 4"));
1744            // A step this build never writes is a defect, but the reader
1745            // still must not panic: EPROTO, as for a malformed length.
1746            let step =
1747                SetupStep::from_wire(wire).ok_or_else(|| wait_syscall_error(Errno::PROTO))?;
1748            Ok(Report::SetupFailed {
1749                step,
1750                errno,
1751                detail,
1752            })
1753        }
1754        // A partial or over-length read: the single-writer protocol was
1755        // broken. EPROTO, so the failure is typed rather than a panic.
1756        _ => Err(wait_syscall_error(Errno::PROTO)),
1757    }
1758}
1759
1760/// Waits for a direct child process to terminate, retrying on signal
1761/// interruption.
1762pub(crate) fn wait_for_exit(pid: Pid) -> Result<WaitStatus, Errno> {
1763    match frame::retry_on_intr!(process::waitpid(Some(pid), WaitOptions::empty()))? {
1764        Some((_, status)) => Ok(status),
1765        None => unreachable!("ferroday-cage: waitpid without WNOHANG returned no status"),
1766    }
1767}
1768
1769fn spawn_error(errno: Errno) -> Error {
1770    Error::Spawn {
1771        source: errno.into(),
1772    }
1773}
1774
1775pub(crate) fn wait_syscall_error(errno: Errno) -> Error {
1776    Error::Wait {
1777        source: errno.into(),
1778    }
1779}
1780
1781#[cfg(test)]
1782mod tests {
1783    use super::*;
1784
1785    /// Builds a status record from its two little-endian words.
1786    fn record(kind: u32, value: i32) -> [u8; 8] {
1787        let mut record = [0u8; 8];
1788        record[0..4].copy_from_slice(&kind.to_le_bytes());
1789        record[4..8].copy_from_slice(&value.to_le_bytes());
1790        record
1791    }
1792
1793    #[test]
1794    fn a_status_record_decodes_both_kinds() {
1795        let exited = decode_status(record(STATUS_EXITED, 42)).expect("a written kind decodes");
1796        assert_eq!(exited.code(), Some(42));
1797        assert_eq!(exited.signal(), None);
1798
1799        let signaled = decode_status(record(STATUS_SIGNALED, 9)).expect("a written kind decodes");
1800        assert_eq!(signaled.code(), None);
1801        assert_eq!(signaled.signal(), Some(9));
1802    }
1803
1804    #[test]
1805    fn an_unknown_status_kind_decodes_to_nothing() {
1806        // The reader turns this into EPROTO rather than panicking: a kind this
1807        // build never writes is a defect, and a defect must not unwind through
1808        // the wait.
1809        assert!(decode_status(record(0, 0)).is_none());
1810        assert!(decode_status(record(99, 0)).is_none());
1811    }
1812}