crates/appliance-pid1/src/lib.rs
//! **The PID-1 duties every distroless nordisk appliance payload owes.**
//!
//! A payload that is PID 1 is not just "the first process". The kernel gives it
//! two jobs nobody else can do:
//!
//! 1. **Reaping.** Every orphaned process on the system is re-parented to PID 1,
//! and PID 1 is the only process that can `wait` for it. A PID 1 that never
//! waits leaves a `<defunct>` entry in the process table for the appliance's
//! whole life, per child, forever.
//! 2. **Not exiting.** If PID 1 returns, the kernel panics.
//!
//! The second one is obvious and every payload does it. The first one is easy to
//! miss precisely because it looks like it works: `loop { pause() }` never
//! returns, so the appliance stays up and the zombies are invisible until you go
//! looking in `/proc`.
//!
//! ## Why this crate exists
//!
//! The shape below was written once, for the demo-container appliance's PID 1,
//! and got it right. The RDP-over-Wayland launcher — which spawns *more*
//! children than the demo track does (weston, an operator-configured app, a
//! keepalive) — ended its `main` with a bare `loop { libc::pause() }` and reaped
//! none of them. Two payloads, one duty, and one of them already had the code.
//! It lives here now so the next payload inherits it instead of re-deriving it.
//!
//! ```no_run
//! fn main() {
//! // … mount, configure, emit the boot contract, start the children …
//! appliance_pid1::idle_and_reap(); // never returns; never a shell
//! }
//! ```
//!
//! ## What this is NOT
//!
//! Reaping is **hygiene, not supervision**. It stops zombies accumulating; it
//! does not notice that the thing the appliance exists to do has stopped, and it
//! has no restart policy. Deciding what an appliance does when its payload dies
//! — restart with backoff, reboot loudly, halt into a known-safe state, pet or
//! stop petting `/dev/watchdog` — is a policy question with safety consequences,
//! and it is deliberately not answered here.
#![deny(missing_docs)]
/// The kernel pseudo-filesystems a PID 1 mounts for itself — with flags, and
/// with its results read.
pub mod mounts;
/// The network a PID 1 brings up itself: netlink, the net spec, the bring-up.
#[cfg(feature = "net")]
#[allow(missing_docs)]
pub mod net;
/// The boot narration's secret door and the kernel tail, handed to a sink.
#[cfg(feature = "net")]
pub mod narration;
/// The DHCPv4 client and its renewal machine.
#[cfg(feature = "dhcp")]
#[allow(missing_docs)]
pub mod dhcp;
/// `/etc/resolv.conf` from a lease's option 6. Under `net`, not `dhcp` (lane G6): a
/// build with no DHCP client still reads what was published (Gunnar's dashboard).
#[cfg(feature = "net")]
#[allow(missing_docs)]
pub mod resolver;
/// The ACPI power button (a cloud's soft stop) and the power-off after it —
/// libc only, like the reaper.
pub mod power;
/// What ends a serve, and what `main` does after a clean stop.
pub mod stop;
/// The install-mode record korp-installer leaves on the ESP.
pub mod install;
/// The machine's graphics console (`/dev/tty0`), appliance only.
#[cfg(feature = "net")]
pub mod console;
/// A file PID 1 keeps, written so a power cut leaves the old one or the new one.
pub mod durable;
/// How a PID 1 that faulted ends: the code, the record, the ear, restart or power off.
pub mod fault;
/// The last stop, kept on the ESP where it survives the read-only /data.
pub mod stop_record;
/// The supervisor reaper that leaves an exited child a grace for its owner (`GraceReaper`).
pub mod grace;
/// Signals, the SIGCHLD reaper and the lease kept alive, on tokio.
#[cfg(feature = "runtime")]
pub mod runtime;
/// What one non-blocking pass of the reaper found.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reaped {
/// A child was reaped. Carries its pid and the raw wait status.
Child {
/// The pid that was reaped.
pid: i32,
/// The raw status word `waitpid` filled in.
status: i32,
},
/// There are children, but none has exited yet. Nothing to do right now.
StillRunning,
/// This process has no children at all (`ECHILD`).
NoChildren,
/// The call was interrupted by a signal (`EINTR`), or failed for another
/// reason. The caller should simply try again.
Interrupted,
/// The child that exited is CLAIMED ([`claim`]): its owner waits for it, so
/// the reaper leaves its status alone. The pass ends here; the owner's
/// `wait` takes it, and the next `SIGCHLD` wakes the reaper again.
Claimed {
/// The pid left for its owner.
pid: i32,
},
}
// ── claimed children ────────────────────────────────────────────────────────
/// The pids whose exit status belongs to a waiter in this process, not to the
/// reaper. MEASURED on Holger's appliance (lane HOLGER-MEDIA, 2026-10-23): the
/// scan sandbox spawns a container runtime and polls `try_wait`; between two
/// polls the reaper's `waitpid(-1)` took the exit, and the owner read `ECHILD`
/// with no status — a finished scan whose verdict is unknown is refused, so
/// every scan was. A claimed pid is peeked at (`WNOWAIT`) and left.
static CLAIMED: std::sync::Mutex<std::collections::BTreeSet<i32>> = std::sync::Mutex::new(std::collections::BTreeSet::new());
/// **Claim a child's exit status for its owner.** Dropping the guard releases
/// the claim (after the owner reaped it; or, if the owner never does, the
/// reaper takes it on its next pass).
pub fn claim(pid: i32) -> Claim {
CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).insert(pid);
Claim(pid)
}
/// A claimed pid ([`claim`]); releases on drop.
#[derive(Debug)]
pub struct Claim(i32);
impl Claim {
/// The claimed pid.
pub fn pid(&self) -> i32 {
self.0
}
}
impl Drop for Claim {
fn drop(&mut self) {
CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).remove(&self.0);
}
}
/// Is `pid` claimed?
pub fn is_claimed(pid: i32) -> bool {
CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).contains(&pid)
}
/// **Reap at most one exited child, without blocking.**
///
/// This is the testable half of the duty: it reports what it found instead of
/// looping, so a test can spawn a real process, let it exit, and prove the
/// zombie is gone — no VM, no boot.
///
/// `waitpid(-1, …, WNOHANG)` asks about *any* child, which is what PID 1 needs:
/// re-parented orphans are children it never spawned and whose pids it does not
/// know.
pub fn reap_once() -> Reaped {
// 1. PEEK (`WNOWAIT`): which child exited, without taking its status.
// SAFETY: `info` is a zeroed `siginfo_t` on this frame, uniquely borrowed;
// `waitid(2)` fills it and keeps no reference. `P_ALL` with id 0 means "any
// child"; `WNOHANG` makes the call non-blocking; `WNOWAIT` leaves the child
// waitable, so an owner's `wait` still gets it.
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::waitid(libc::P_ALL, 0, &mut info, libc::WEXITED | libc::WNOHANG | libc::WNOWAIT) };
if rc < 0 {
// Distinguish "no children" from "try again", because the caller
// sleeps on the first and retries on the second.
return match std::io::Error::last_os_error().raw_os_error() {
Some(libc::ECHILD) => Reaped::NoChildren,
_ => Reaped::Interrupted,
};
}
// SAFETY: `si_pid` reads the union field `waitid` fills for `CLD_*` codes;
// with `WNOHANG` and nothing exited it is 0 (the struct was zeroed).
let pid = unsafe { info.si_pid() };
if pid == 0 {
// WNOHANG: children exist, none has changed state.
return Reaped::StillRunning;
}
if is_claimed(pid) {
return Reaped::Claimed { pid };
}
// 2. REAP exactly that pid.
let mut status: libc::c_int = 0;
// SAFETY: the only pointer is `&mut status`, a live, uniquely borrowed
// `c_int` on this frame, and `waitpid(2)` writes at most one `int` through
// it and keeps no reference. `WNOHANG` makes the call non-blocking.
let rc = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
if rc == pid {
return Reaped::Child { pid, status };
}
if rc == 0 {
return Reaped::StillRunning;
}
match std::io::Error::last_os_error().raw_os_error() {
Some(libc::ECHILD) => Reaped::NoChildren,
_ => Reaped::Interrupted,
}
}
/// **Reap every child that has already exited, and report which** — the
/// drain [`reap_once`] is the unit of, as the pids (gunnar-server's
/// `pid1::reap_once`, now this crate's libc reaper under it: one reaper).
///
/// Returns the raw pids, **not** a count and not a bool: a reaper that returns
/// `true` cannot be told from one that returns `true` unconditionally. Never
/// blocks: `WNOHANG` throughout, so on a process with no children it costs
/// one `waitpid` returning `ECHILD`.
pub fn reap_all() -> Vec<i32> {
let mut reaped = Vec::new();
// `Interrupted` is also "any other error": retried a few times, never spun on.
let mut interrupted = 0;
loop {
match reap_once() {
Reaped::Child { pid, .. } => reaped.push(pid),
Reaped::Interrupted if interrupted < 8 => interrupted += 1,
Reaped::Interrupted => return reaped,
// Its owner waits for it; the next SIGCHLD brings the reaper back.
Reaped::Claimed { .. } => return reaped,
Reaped::StillRunning | Reaped::NoChildren => return reaped,
}
}
}
/// **The PID-1 duty: reap forever, and never return.**
///
/// Drains every child that has already exited, then blocks in `pause()` until a
/// signal arrives — `SIGCHLD` from the next child to die is one of them, which
/// is exactly the wake-up this loop wants. It never busy-spins and it never
/// starts a shell.
///
/// Diverges: an appliance's `main` ends here.
pub fn idle_and_reap() -> ! {
loop {
// Drain: one SIGCHLD can stand for several dead children.
while let Reaped::Child { .. } = reap_once() {}
// Nothing left to reap right now — sleep until a signal, rather than
// spinning a core for the life of the appliance.
// SAFETY: `pause(2)` takes no arguments and touches no memory. It blocks
// until a signal arrives (SIGCHLD from the next dead child is one) and
// always returns `-1`/`EINTR`, so the loop is what makes this diverge.
unsafe { libc::pause() };
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The whole point, proven against a real process rather than a mock: a
/// child whose handle nobody holds becomes a **zombie** — `/proc/<pid>/stat`
/// says `Z` — and stays one until PID 1 waits for it.
///
/// One test function does the whole sequence on purpose. `waitpid(-1, …)`
/// asks about *any* child of this process, so two tests reaping concurrently
/// in the same test binary would steal each other's children.
#[test]
fn a_forgotten_child_is_a_zombie_until_it_is_reaped() {
// `true` is not available in a distroless image, but this test runs on a
// developer host; the crate under test spawns nothing itself.
let child = std::process::Command::new("/bin/sh")
.args(["-c", "exit 7"])
.spawn()
.expect("spawn a short-lived child");
let pid = child.id() as i32;
// Drop the handle WITHOUT waiting — this is precisely what
// `Ok(_child) => { … }` does at a spawn site, and `Child::drop` does not
// wait either. Nothing but a reaper will ever collect it.
std::mem::forget(child);
// Wait for it to actually die and become a zombie.
let mut sawstate = None;
for _ in 0..400 {
match proc_state(pid) {
Some('Z') => {
sawstate = Some('Z');
break;
}
other => sawstate = other,
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert_eq!(
sawstate,
Some('Z'),
"the child never became a zombie, so this test proves nothing about reaping"
);
// Now do PID 1's job.
let reaped = reap_once();
match reaped {
Reaped::Child { pid: got, status } => {
assert_eq!(got, pid, "reaped the wrong child");
assert!(libc::WIFEXITED(status), "status word: {status}");
assert_eq!(libc::WEXITSTATUS(status), 7, "the exit code survives");
}
other => panic!("expected to reap {pid}, got {other:?}"),
}
// And it is gone from the process table.
assert_eq!(
proc_state(pid),
None,
"pid {pid} is still in /proc after being reaped"
);
// A CLAIMED child (same sequence, same reason: one reaper per test
// binary): the reaper peeks, names it, and leaves its status to the
// owner, whose `wait` still gets the exit code.
let mut owned = std::process::Command::new("/bin/sh")
.args(["-c", "exit 3"])
.spawn()
.expect("spawn an owned child");
let owned_pid = owned.id() as i32;
let guard = claim(owned_pid);
assert!(is_claimed(owned_pid));
for _ in 0..400 {
if proc_state(owned_pid) == Some('Z') {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert_eq!(proc_state(owned_pid), Some('Z'), "the owned child never exited");
assert_eq!(reap_once(), Reaped::Claimed { pid: owned_pid }, "the reaper leaves a claimed child");
assert_eq!(reap_all(), Vec::<i32>::new(), "a pass ends at a claimed child");
assert_eq!(proc_state(owned_pid), Some('Z'), "still waitable by its owner");
let st = owned.wait().expect("the owner's wait gets the status");
assert_eq!(st.code(), Some(3));
drop(guard);
assert!(!is_claimed(owned_pid));
}
/// With no children at all the reaper says so, so `idle_and_reap` sleeps
/// instead of spinning. This is the state a healthy appliance sits in.
#[test]
fn no_children_is_reported_as_such_and_is_not_an_error() {
// Deliberately not asserted on its own — the reaping test above may have
// a child in flight — but the variant must exist and be distinguishable.
let r = reap_once();
assert!(
matches!(
r,
Reaped::NoChildren | Reaped::StillRunning | Reaped::Child { .. }
),
"unexpected {r:?}"
);
}
/// The single-character state field of `/proc/<pid>/stat`, or `None` when the
/// pid is gone. Parsed from the LAST `)` because the comm field can contain
/// spaces and parentheses.
fn proc_state(pid: i32) -> Option<char> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let after_comm = &stat[stat.rfind(')')? + 1..];
after_comm.split_whitespace().next()?.chars().next()
}
}