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}