Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/sandbox/src/crun.rs

//! **The direct OCI-runtime engine: `crun`, with Holger writing the bundle itself**
//! (lane HOLGER-MEDIA, 2026-10-23).
//!
//! The appliance is OS-less: korp-start `fexecve`s the static holger-server as
//! PID 1, and there is no shell, no setuid helper, no `/etc/subuid`, no image
//! store. Rootless podman would bring a userland the medium does not have
//! (podman ≈ 45 MB of Go plus conmon, newuidmap/newgidmap, containers.conf,
//! storage.conf, policy.json, an overlay store and a second user). crun is ONE
//! static program of 1.9 MB (`crun-<ver>-linux-amd64-disable-systemd`), and the
//! OCI bundle is a directory with a `config.json`, which this module writes.
//! Every limit the podman engine sets holds, in the runtime's own vocabulary:
//!
//! | What | How |
//! |---|---|
//! | No network | a new, empty network namespace (no interface, not even lo up) |
//! | The artifact, the job, the extra inputs | bind mounts, `ro,nosuid,nodev,noexec` |
//! | A scratch space with a cap | tmpfs `/scratch`, `size=<n>m,mode=1777,noexec,nosuid,nodev` |
//! | CPU, memory, pids | cgroup v2: `cpu.max`, `memory.max` (`memory.swap.max 0`), `pids.max` |
//! | A wall-clock timeout | the runtime's child is killed at the deadline, the container killed and deleted |
//! | A read-only rootfs | `root.readonly`, plus the masked and read-only `/proc` paths podman sets |
//! | No capabilities | every capability set empty (bounding set too) |
//! | No privilege gain | `noNewPrivileges` |
//! | Seccomp | the containers default profile (`seccomp/containers-default.json`), the rules a container with no capability gets |
//! | A non-root user | `process.user` = the configured uid:gid |
//! | No host root inside | a user namespace: container uids map to `userns_base..+65536` on the host, never to host 0 |
//!
//! `require_rootless` means, here, "the container runs in a user namespace with
//! no host uid 0 in its map": a kernel without `CONFIG_USER_NS` is refused as
//! [`Refusal::NotRootless`]. Images are not loaded into a store: the worker is a
//! directory on disk (`rootfs`), laid from the medium and verified by sha256
//! before the server serves (holger-server `sandbox::media`).
//!
//! The runtime's stdout is the worker's: `crun run` in the foreground inherits
//! the container's stdio, so [`crate::drive`] reads it exactly as it reads
//! podman's.

use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

use crate::{drive, Drive, Engine, Limits, Output, Refusal, SandboxConfig};

/// The runtime program a default config names.
pub const DEFAULT_CRUN: &str = "crun";
/// The worker program inside its rootfs (the image's `ENTRYPOINT`).
pub const WORKER_BIN: &str = "/holger-scan-worker";
/// The worker's default argument (the image's `CMD`): the inputs directory.
pub const WORKER_ARG: &str = "/in";
/// The first host uid a container's uid 0 maps to. Container uid 65532 is host
/// `base + 65532`; host 0 is never in the map.
pub const DEFAULT_USERNS_BASE: u32 = 100_000;
/// How many ids one container's user namespace maps.
pub const USERNS_SIZE: u32 = 65_536;
/// The cgroup every sandbox container is made under (`/sys/fs/cgroup/<this>/<name>`).
pub const CGROUP_PARENT: &str = "holger-sandbox";
/// Where cgroup v2 is mounted.
pub const CGROUP_ROOT: &str = "/sys/fs/cgroup";
/// The containers default seccomp profile (containers/common, Apache-2.0;
/// `LICENSES/Apache-2.0.txt`). podman applies it to every container; this
/// engine applies the rules a container with no capability gets.
pub const SECCOMP_PROFILE_JSON: &str = include_str!("../seccomp/containers-default.json");

/// The OCI runtime-spec version the bundle declares.
const OCI_VERSION: &str = "1.0.2";

// ── the spec ────────────────────────────────────────────────────────────────

/// One mount of a container.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MountSpec {
    /// A host path, bind-mounted (read-only unless `rw`; always nosuid, nodev, noexec).
    Bind {
        host: PathBuf,
        dest: String,
        rw: bool,
    },
    /// A tmpfs with a size cap.
    Tmpfs {
        dest: String,
        size_mib: u64,
        mode: u32,
    },
}

/// What one container is, as data. [`oci_config`] renders it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spec {
    pub name: String,
    pub hostname: String,
    /// The root filesystem (a directory on the host), mounted read-only.
    pub rootfs: PathBuf,
    pub argv: Vec<String>,
    pub env: Vec<(String, String)>,
    pub uid: u32,
    pub gid: u32,
    pub limits: Limits,
    pub mounts: Vec<MountSpec>,
    pub userns_base: u32,
    /// Unshare the IPC namespace too (needs `CONFIG_IPC_NS`; [`Kernel::ipc`]).
    pub ipc: bool,
    /// The program's own GENERATED seccomp profile (OCI `linux.seccomp`), instead of the
    /// capability-free containers default ([`seccomp_profile`]); clamd's
    /// ([`clamd_seccomp_profile`]). `None`: the default.
    pub seccomp: Option<serde_json::Value>,
}

/// What [`Crun::ready`] learned about the kernel.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Kernel {
    pub ipc: bool,
}

impl Kernel {
    /// Look at `/proc/self/ns`: which namespaces this kernel has.
    pub fn probe() -> Result<Kernel, Refusal> {
        let ns = |n: &str| Path::new("/proc/self/ns").join(n).exists();
        if !ns("user") {
            return Err(Refusal::NotRootless);
        }
        for n in ["pid", "mnt", "net", "uts"] {
            if !ns(n) {
                return Err(Refusal::NoEngine(format!(
                    "the kernel has no {n} namespace (CONFIG_NAMESPACES)"
                )));
            }
        }
        Ok(Kernel { ipc: ns("ipc") })
    }
}

/// Which `cgroup.controllers` the sandbox needs at [`CGROUP_ROOT`].
pub const CONTROLLERS: [&str; 3] = ["cpu", "memory", "pids"];

/// Is cgroup v2 mounted at `root` with every controller the limits need?
pub fn cgroup_ready(root: &Path) -> Result<(), Refusal> {
    let f = root.join("cgroup.controllers");
    let have = std::fs::read_to_string(&f).map_err(|e| {
        Refusal::NoEngine(format!(
            "cgroup v2 is not mounted at {} ({e}); the limits need it",
            root.display()
        ))
    })?;
    let have: Vec<&str> = have.split_whitespace().collect();
    let missing: Vec<&str> = CONTROLLERS
        .iter()
        .copied()
        .filter(|c| !have.contains(c))
        .collect();
    if !missing.is_empty() {
        return Err(Refusal::NoEngine(format!(
            "cgroup v2 at {} lacks the {} controller(s) (CONFIG_CGROUPS, MEMCG, CGROUP_PIDS, CFS_BANDWIDTH)",
            root.display(),
            missing.join(", ")
        )));
    }
    Ok(())
}

/// **The seccomp profile** this engine applies: the containers default profile
/// reduced to what applies to a container holding no capability, in the OCI
/// runtime-spec shape (`linux.seccomp`). Rules that need a capability
/// (`includes.caps`) are dropped; rules that podman lifts only with a
/// capability (`excludes.caps`) stay, since none is held; the amd64 rules stay.
pub fn seccomp_profile() -> serde_json::Value {
    static PROFILE: std::sync::OnceLock<serde_json::Value> = std::sync::OnceLock::new();
    PROFILE
        .get_or_init(|| {
            reduce_profile(
                &serde_json::from_str(SECCOMP_PROFILE_JSON).expect("the vendored profile is JSON"),
            )
        })
        .clone()
}

/// **clamd's GENERATED seccomp profile** (lane HEIMDALL-ALLIN prof): what `heimdall profile
/// gen` (Heimdall 0.3, heimdall-probe) wrote from what clamd really called in learning mode,
/// in the shape it runs in here, serving Holger (PING, INSTREAM of real packages and the
/// EICAR test string, RELOAD, bad clients, SelfCheck); `seccomp/clamd/README.md` has the
/// corpus. Unknown call: the process is killed (the supervisor starts clamd again; the
/// required ClamAV scanner refuses every scan in between).
pub const CLAMD_SECCOMP_JSON: &str = include_str!("../seccomp/clamd/x86_64-musl.oci.json");

/// Its pin: `sha256:` of [`CLAMD_SECCOMP_JSON`] (a changed profile is a reviewed diff of both).
pub const CLAMD_SECCOMP_SHA256: &str =
    "sha256:4a714c3dac4d4ac393accf15a5ba788120a58af1649a6a51583e890cc4e19f53";

/// **The calls the runtime makes after it installed the filter**, before clamd's first
/// instruction: the generated profile starts at clamd's own first call (Heimdall's probe
/// starts the program by `execveat` and records from there); crun, right after loading the
/// filter, closes its descriptors (`close_range`) and starts clamd by `execve` (measured:
/// without `close_range` crun is killed before clamd runs). These, and only these, are added.
pub const RUNTIME_AFTER_FILTER: &[&str] = &["close_range", "execve"];

/// clamd's profile as crun applies it: [`CLAMD_SECCOMP_JSON`] plus [`RUNTIME_AFTER_FILTER`].
pub fn clamd_seccomp_profile() -> serde_json::Value {
    let mut v: serde_json::Value =
        serde_json::from_str(CLAMD_SECCOMP_JSON).expect("the checked-in profile is JSON");
    let calls = v["syscalls"]
        .as_array_mut()
        .expect("an OCI seccomp profile");
    calls.push(serde_json::json!({
        "names": RUNTIME_AFTER_FILTER,
        "action": "SCMP_ACT_ALLOW",
    }));
    v
}

/// **Never allowed in the sandbox, whatever the default profile says**: the
/// containers default profile lists these in its unconditional allow set and
/// relies on the missing capability to refuse them at the call. The sandbox
/// holds no capability and never will, so they leave the filter too (ENOSYS,
/// the profile's default): one fewer kernel entry point for a hostile artifact
/// to reach, and a tightening over podman's default, never a loosening.
pub const NEVER_WITHOUT_CAPABILITIES: &[&str] = &[
    "mount",
    "umount",
    "umount2",
    "unshare",
    "setns",
    "pivot_root",
    "chroot",
    "mount_setattr",
    "open_tree",
    "move_mount",
    "fsopen",
    "fsconfig",
    "fsmount",
    "fspick",
    "reboot",
    "swapon",
    "swapoff",
    "kexec_load",
    "kexec_file_load",
    "init_module",
    "finit_module",
    "delete_module",
    "query_module",
    "ptrace",
    "process_vm_readv",
    "process_vm_writev",
    "kcmp",
    "process_madvise",
    "pidfd_getfd",
    "sethostname",
    "setdomainname",
    "settimeofday",
    "clock_settime",
    "clock_settime64",
    "stime",
    "adjtimex",
    "clock_adjtime",
    "clock_adjtime64",
    "iopl",
    "ioperm",
    "vhangup",
    "acct",
    "quotactl",
    "quotactl_fd",
    "nfsservctl",
    "bpf",
    "perf_event_open",
    "userfaultfd",
    "lookup_dcookie",
    "add_key",
    "keyctl",
    "request_key",
    "open_by_handle_at",
    "name_to_handle_at",
    "syslog",
    "vm86",
    "vm86old",
    "modify_ldt",
    "uselib",
    "ustat",
    "sysfs",
    "nfsservctl",
];

fn reduce_profile(p: &serde_json::Value) -> serde_json::Value {
    const ARCH: &str = "amd64";
    let list = |v: &serde_json::Value| -> Vec<String> {
        v.as_array()
            .map(|a| {
                a.iter()
                    .filter_map(|x| x.as_str().map(str::to_string))
                    .collect()
            })
            .unwrap_or_default()
    };
    let mut syscalls = Vec::new();
    for s in p["syscalls"].as_array().into_iter().flatten() {
        let inc_caps = list(&s["includes"]["caps"]);
        let inc_arches = list(&s["includes"]["arches"]);
        let exc_arches = list(&s["excludes"]["arches"]);
        if !inc_caps.is_empty()
            || (!inc_arches.is_empty() && !inc_arches.iter().any(|a| a == ARCH))
            || exc_arches.iter().any(|a| a == ARCH)
        {
            continue;
        }
        let names: Vec<serde_json::Value> = s["names"]
            .as_array()
            .into_iter()
            .flatten()
            .filter(|n| {
                !(s["action"] == "SCMP_ACT_ALLOW"
                    && n.as_str()
                        .is_some_and(|n| NEVER_WITHOUT_CAPABILITIES.contains(&n)))
            })
            .cloned()
            .collect();
        if names.is_empty() {
            continue;
        }
        let mut rule = serde_json::json!({ "names": names, "action": s["action"] });
        if let Some(e) = s.get("errnoRet") {
            rule["errnoRet"] = e.clone();
        }
        if let Some(a) = s
            .get("args")
            .filter(|a| a.as_array().is_some_and(|x| !x.is_empty()))
        {
            rule["args"] = a.clone();
        }
        syscalls.push(rule);
    }
    let mut arches = Vec::new();
    for m in p["archMap"].as_array().into_iter().flatten() {
        if m["architecture"] == "SCMP_ARCH_X86_64" {
            arches.push(m["architecture"].clone());
            arches.extend(
                m["subArchitectures"]
                    .as_array()
                    .into_iter()
                    .flatten()
                    .cloned(),
            );
        }
    }
    let mut out = serde_json::json!({ "defaultAction": p["defaultAction"], "architectures": arches, "syscalls": syscalls });
    if let Some(e) = p.get("defaultErrnoRet") {
        out["defaultErrnoRet"] = e.clone();
    }
    out
}

/// **The bundle's `config.json`** — pure, so every hardening line is tested
/// without a runtime.
pub fn oci_config(spec: &Spec) -> serde_json::Value {
    let l = &spec.limits;
    let mem = (l.memory_mib << 20) as i64;
    let mut mounts = vec![
        serde_json::json!({ "destination": "/proc", "type": "proc", "source": "proc",
            "options": ["nosuid", "noexec", "nodev"] }),
        serde_json::json!({ "destination": "/dev", "type": "tmpfs", "source": "tmpfs",
            "options": ["nosuid", "noexec", "strictatime", "mode=755", "size=65536k"] }),
    ];
    for m in &spec.mounts {
        mounts.push(match m {
            MountSpec::Bind { host, dest, rw } => serde_json::json!({
                "destination": dest, "type": "bind", "source": host,
                "options": ["rbind", if *rw { "rw" } else { "ro" }, "nosuid", "nodev", "noexec"],
            }),
            MountSpec::Tmpfs { dest, size_mib, mode } => serde_json::json!({
                "destination": dest, "type": "tmpfs", "source": "tmpfs",
                "options": ["rw", format!("size={size_mib}m"), format!("mode={mode:o}"), "noexec", "nosuid", "nodev"],
            }),
        });
    }
    let mut namespaces = vec![
        serde_json::json!({ "type": "pid" }),
        serde_json::json!({ "type": "mount" }),
        serde_json::json!({ "type": "network" }),
        serde_json::json!({ "type": "uts" }),
        serde_json::json!({ "type": "user" }),
    ];
    if spec.ipc {
        namespaces.push(serde_json::json!({ "type": "ipc" }));
    }
    let env: Vec<String> = spec.env.iter().map(|(k, v)| format!("{k}={v}")).collect();
    let map =
        serde_json::json!([{ "containerID": 0, "hostID": spec.userns_base, "size": USERNS_SIZE }]);
    serde_json::json!({
        "ociVersion": OCI_VERSION,
        "hostname": spec.hostname,
        "process": {
            "terminal": false,
            "user": { "uid": spec.uid, "gid": spec.gid },
            "args": spec.argv,
            "env": env,
            "cwd": "/",
            "capabilities": { "bounding": [], "effective": [], "inheritable": [], "permitted": [], "ambient": [] },
            "noNewPrivileges": true,
            "rlimits": [{ "type": "RLIMIT_NOFILE", "hard": 4096, "soft": 4096 }],
        },
        "root": { "path": spec.rootfs, "readonly": true },
        "mounts": mounts,
        "linux": {
            "namespaces": namespaces,
            "uidMappings": map,
            "gidMappings": map,
            "resources": {
                "memory": { "limit": mem, "swap": mem },
                "cpu": { "quota": (l.cpu_millis * 100) as i64, "period": 100_000 },
                "pids": { "limit": l.pids as i64 },
            },
            "cgroupsPath": format!("/{CGROUP_PARENT}/{}", spec.name),
            "seccomp": spec.seccomp.clone().unwrap_or_else(seccomp_profile),
            "rootfsPropagation": "private",
            "maskedPaths": ["/proc/acpi", "/proc/kcore", "/proc/keys", "/proc/latency_stats", "/proc/timer_list",
                "/proc/timer_stats", "/proc/sched_debug", "/proc/scsi", "/sys/firmware", "/sys/dev/block"],
            "readonlyPaths": ["/proc/asound", "/proc/bus", "/proc/fs", "/proc/irq", "/proc/sys", "/proc/sysrq-trigger"],
        },
    })
}

// ── the bundle on disk, and the runtime's verbs ─────────────────────────────

/// Where one engine keeps its bundles and the runtime's own state.
#[derive(Debug, Clone)]
pub struct State {
    pub dir: PathBuf,
}

impl State {
    pub fn bundles(&self) -> PathBuf {
        self.dir.join("bundles")
    }
    /// crun's `--root`: its per-container state, under ours (never `/run/crun`).
    pub fn runtime_root(&self) -> PathBuf {
        self.dir.join("run")
    }
    /// Write the bundle of `spec`; its directory.
    pub fn write_bundle(&self, spec: &Spec) -> Result<PathBuf, Refusal> {
        let dir = self.bundles().join(&spec.name);
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir)
            .map_err(|e| Refusal::Io(format!("{}: {e}", dir.display())))?;
        let cfg = serde_json::to_vec_pretty(&oci_config(spec))
            .map_err(|e| Refusal::Io(format!("config.json: {e}")))?;
        std::fs::write(dir.join("config.json"), cfg)
            .map_err(|e| Refusal::Io(format!("{}/config.json: {e}", dir.display())))?;
        Ok(dir)
    }
    pub fn remove_bundle(&self, name: &str) {
        let _ = std::fs::remove_dir_all(self.bundles().join(name));
    }
}

/// The runtime program and its state: the verbs this engine uses.
#[derive(Debug, Clone)]
pub struct Runtime {
    pub program: String,
    pub state: State,
}

impl Runtime {
    fn cmd(&self) -> Command {
        // A32-14, a process by design: the sandbox runs the OCI runtime (crun) it is configured with; the sandbox is the feature.
        let mut c = Command::new(&self.program);
        c.arg("--root")
            .arg(self.state.runtime_root())
            .arg("--cgroup-manager=cgroupfs");
        c.stdin(Stdio::null());
        c
    }

    fn quiet(&self, args: &[&str]) -> Result<std::process::Output, Refusal> {
        claimed_output(|| {
            let mut c = self.cmd();
            c.args(args);
            c
        })
        .map_err(|e| Refusal::NoEngine(format!("{} {}: {e}", self.program, args.join(" "))))
    }

    /// `crun --version`: is the program there?
    pub fn version(&self) -> Result<String, Refusal> {
        let o = claimed_output(|| {
            // A32-14, a process by design: the sandbox runs the OCI runtime (crun) it is configured with; the sandbox is the feature.
            let mut c = Command::new(&self.program);
            c.arg("--version").stdin(Stdio::null());
            c
        })
        .map_err(|e| Refusal::NoEngine(format!("{} --version: {e}", self.program)))?;
        if !o.status.success() {
            return Err(Refusal::NoEngine(format!(
                "{} --version exited {}",
                self.program, o.status
            )));
        }
        Ok(String::from_utf8_lossy(&o.stdout)
            .lines()
            .next()
            .unwrap_or_default()
            .to_string())
    }

    /// The `run` arguments for `bundle` as `name`.
    ///
    /// `--no-pivot`: MEASURED on the laid box (2026-10-24), `pivot_root: Invalid
    /// argument` from both containers. The appliance's root IS the initramfs
    /// (`rootfs`), which pivot_root cannot leave; the runtime chroots into the
    /// rootfs instead — inside its own mount namespace, with every capability
    /// gone and `chroot`/`pivot_root`/`mount` out of the seccomp filter, that is
    /// the same wall. `--no-new-keyring`: the kernel has no `CONFIG_KEYS`.
    pub fn run_args(name: &str, bundle: &Path) -> Vec<String> {
        vec![
            "run".into(),
            "--no-new-keyring".into(),
            "--no-pivot".into(),
            "--bundle".into(),
            bundle.display().to_string(),
            name.into(),
        ]
    }

    /// Start `spec` in the foreground: the child's stdout and stderr are the
    /// container's. The bundle is written first.
    pub fn spawn(&self, spec: &Spec) -> Result<std::process::Child, Refusal> {
        let bundle = self.state.write_bundle(spec)?;
        self.cmd()
            .args(Self::run_args(&spec.name, &bundle))
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            // A32-14, a process by design: the OCI runtime process (see its constructor).
            .spawn()
            .map_err(|e| Refusal::NoEngine(format!("{} run: {e}", self.program)))
    }

    /// Kill the container by name (SIGKILL), whatever state it is in.
    pub fn kill(&self, name: &str) {
        let _ = self.quiet(&["kill", name, "KILL"]);
    }

    /// Delete the container's state and its bundle.
    pub fn delete(&self, name: &str) {
        let _ = self.quiet(&["delete", "--force", name]);
        self.state.remove_bundle(name);
    }

    /// Did the container's cgroup record an OOM kill? Read before `delete`.
    pub fn oom_killed(&self, name: &str) -> bool {
        oom_killed_in(Path::new(CGROUP_ROOT), name)
    }
}

/// **A short runtime call run to its end with its child claimed** from a PID 1's reaper
/// ([`crate::claim_child`]), as `spawn` claims the scan's: on the laid box holger is PID 1, and an
/// unclaimed child's exit could be reaped first (`ECHILD`, release R2 2026-10-25). The claim
/// lands right after the spawn; an `ECHILD` that still slips in before it is tried again, twice.
fn claimed_output(make: impl Fn() -> Command) -> std::io::Result<std::process::Output> {
    let mut last = None;
    for _ in 0..3 {
        // A32-14, a process by design: the OCI runtime process (see its constructor).
        let child = make()
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;
        let _claim = crate::claim_child(&child);
        match child.wait_with_output() {
            Err(e) if e.raw_os_error() == Some(ECHILD) => last = Some(e),
            r => return r,
        }
    }
    Err(last.unwrap_or_else(|| std::io::Error::from_raw_os_error(ECHILD)))
}

/// `ECHILD` on Linux: the child's status was taken by another waiter.
const ECHILD: i32 = 10;

fn oom_killed_in(cgroup_root: &Path, name: &str) -> bool {
    let f = cgroup_root
        .join(CGROUP_PARENT)
        .join(name)
        .join("memory.events");
    std::fs::read_to_string(f)
        .ok()
        .and_then(|t| {
            t.lines().find_map(|l| {
                l.strip_prefix("oom_kill ")
                    .and_then(|n| n.trim().parse::<u64>().ok())
            })
        })
        .is_some_and(|n| n > 0)
}

// ── the engine ──────────────────────────────────────────────────────────────

/// The engine: the local `crun` program over a rootfs directory.
#[derive(Debug, Default)]
pub struct Crun {
    kernel: std::sync::OnceLock<Kernel>,
}

impl Crun {
    pub fn runtime(cfg: &SandboxConfig) -> Result<Runtime, Refusal> {
        let dir = cfg
            .state_dir
            .clone()
            .or_else(|| cfg.work_dir.as_ref().map(|w| format!("{w}/crun")))
            .map(PathBuf::from)
            .unwrap_or_else(|| std::env::temp_dir().join("holger-sandbox-crun"));
        std::fs::create_dir_all(&dir)
            .map_err(|e| Refusal::Io(format!("{}: {e}", dir.display())))?;
        Ok(Runtime {
            program: cfg.crun.clone(),
            state: State { dir },
        })
    }

    fn rootfs(cfg: &SandboxConfig) -> Result<PathBuf, Refusal> {
        let r = cfg.rootfs.as_deref().ok_or_else(|| {
            Refusal::Config("engine `crun` needs `rootfs`: the worker's root directory".into())
        })?;
        let r = PathBuf::from(r);
        if !r.is_absolute() {
            return Err(Refusal::Config(format!(
                "rootfs {} is not an absolute path",
                r.display()
            )));
        }
        Ok(r)
    }

    /// The worker's spec for one run: the decoded `run` arguments over the config.
    pub fn spec(
        cfg: &SandboxConfig,
        name: &str,
        mounts: &[(String, String)],
        command: &[String],
        ipc: bool,
    ) -> Result<Spec, Refusal> {
        let (uid, gid) = parse_user(&cfg.user)?;
        let mut argv = vec![WORKER_BIN.to_string()];
        if command.is_empty() {
            argv.push(WORKER_ARG.to_string());
        } else {
            argv.extend(command.iter().cloned());
        }
        let mut m = vec![MountSpec::Tmpfs {
            dest: crate::SCRATCH_PATH.into(),
            size_mib: cfg.limits.scratch_mib,
            mode: 0o1777,
        }];
        m.extend(mounts.iter().map(|(h, c)| MountSpec::Bind {
            host: PathBuf::from(h),
            dest: c.clone(),
            rw: false,
        }));
        Ok(Spec {
            name: name.to_string(),
            hostname: "holger-scan".into(),
            rootfs: Self::rootfs(cfg)?,
            argv,
            env: vec![
                ("HOME".into(), crate::SCRATCH_PATH.into()),
                ("TMPDIR".into(), crate::SCRATCH_PATH.into()),
                ("PATH".into(), "/".into()),
            ],
            uid,
            gid,
            limits: cfg.limits.clone(),
            mounts: m,
            userns_base: cfg.userns_base,
            ipc,
            seccomp: None,
        })
    }
}

impl Crun {
    /// The worker's spec for one run, from the `run` arguments themselves: their
    /// read-only mounts, the worker's own argv, and a job's writable output
    /// directory at [`crate::OUT_PATH`] (`rw`, still nosuid, nodev, noexec).
    pub fn spec_for(
        cfg: &SandboxConfig,
        name: &str,
        args: &[String],
        ipc: bool,
    ) -> Result<Spec, Refusal> {
        let mut spec = Self::spec(
            cfg,
            name,
            &crate::mounts_of(args),
            &crate::command_of(cfg, args),
            ipc,
        )?;
        if let Some(out) = crate::out_of(args) {
            spec.mounts.push(MountSpec::Bind {
                host: PathBuf::from(out),
                dest: crate::OUT_PATH.into(),
                rw: true,
            });
        }
        Ok(spec)
    }
}

/// `uid:gid` (or `uid`) as numbers.
pub fn parse_user(user: &str) -> Result<(u32, u32), Refusal> {
    let (u, g) = user.split_once(':').unwrap_or((user, user));
    let uid = u
        .parse::<u32>()
        .map_err(|_| Refusal::Config(format!("user {user:?} is not numeric")))?;
    let gid = g
        .parse::<u32>()
        .map_err(|_| Refusal::Config(format!("user {user:?} is not numeric")))?;
    Ok((uid, gid))
}

impl Engine for Crun {
    fn ready(&self, cfg: &SandboxConfig) -> Result<(), Refusal> {
        let rootfs = Self::rootfs(cfg)?;
        if !rootfs.join(WORKER_BIN.trim_start_matches('/')).is_file() {
            return Err(Refusal::ImageMissing(format!(
                "{} (no {WORKER_BIN} in it)",
                rootfs.display()
            )));
        }
        let rt = Self::runtime(cfg)?;
        rt.version()?;
        let kernel = Kernel::probe()?;
        if cfg.require_rootless && cfg.userns_base == 0 {
            return Err(Refusal::NotRootless);
        }
        cgroup_ready(Path::new(CGROUP_ROOT))?;
        let _ = self.kernel.set(kernel);
        Ok(())
    }

    fn run(&self, cfg: &SandboxConfig, name: &str, args: &[String]) -> Result<Output, Refusal> {
        let kernel = *self.kernel.get().unwrap_or(&Kernel::default());
        let spec = Self::spec_for(cfg, name, args, kernel.ipc)?;
        let rt = Self::runtime(cfg)?;
        let started = Instant::now();
        // A32-14, a process by design: the sandboxed program, started by the OCI runtime, claimed and reaped.
        let child = rt.spawn(&spec)?;
        let _claim = crate::claim_child(&child);
        let deadline = started + Duration::from_secs(cfg.limits.timeout_secs);
        let r = drive(child, deadline, cfg.limits.report_max_bytes);
        let out = match r {
            Drive::Done {
                status,
                stdout,
                stderr,
            } => {
                let code = status.code();
                if status.success() {
                    Ok(Output {
                        report: stdout,
                        elapsed: started.elapsed(),
                    })
                } else {
                    let oom = rt.oom_killed(name);
                    if code == Some(137) || code == Some(139) || code.is_none() || oom {
                        Err(Refusal::Killed {
                            exit: code,
                            oom,
                            stderr,
                        })
                    } else {
                        Err(Refusal::WorkerFailed { exit: code, stderr })
                    }
                }
            }
            Drive::TimedOut => {
                rt.kill(name);
                Err(Refusal::Timeout {
                    secs: cfg.limits.timeout_secs,
                })
            }
            Drive::TooLarge => {
                rt.kill(name);
                Err(Refusal::ReportTooLarge {
                    cap: cfg.limits.report_max_bytes,
                })
            }
            Drive::Io(e) => {
                rt.kill(name);
                Err(Refusal::Io(e))
            }
        };
        rt.delete(name);
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    static CLAIMED: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());

    fn record_claim(pid: i32) -> crate::ChildClaim {
        CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).push(pid);
        Box::new(())
    }

    /// ★ **`crun --version` is claimed from a PID 1's reaper** (release R2, 2026-10-25): on the
    /// laid box holger is PID 1, and an unclaimed `--version` child lost its exit to the reaper
    /// (`ECHILD`) — the scan worker's first `ready()` then said `sandbox-no-engine` and kept
    /// saying it (the readiness is cached), so every scan of the box was refused. MEASURED in
    /// the R2 installtest of holger 919d2416 (one RED run in two). The calls that only ask
    /// (`--version`, `kill`, `delete`, `state`) claim their child as `spawn` does.
    #[test]
    fn the_runtimes_own_short_calls_claim_their_child() {
        crate::set_child_claim(record_claim);
        let dir = tempfile::tempdir().unwrap();
        let rt = Runtime {
            program: "/bin/echo".into(),
            state: State {
                dir: dir.path().to_path_buf(),
            },
        };
        let before = CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).len();
        assert!(rt.version().is_ok(), "{:?}", rt.version());
        let after = CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).len();
        assert!(after > before, "the --version child was not claimed");
        rt.kill("holger-scan-x");
        assert!(
            CLAIMED.lock().unwrap_or_else(|e| e.into_inner()).len() > after,
            "the kill child was not claimed"
        );
    }

    fn cfg() -> SandboxConfig {
        SandboxConfig {
            engine: crate::EngineKind::Crun,
            rootfs: Some("/data/holger/sandbox/worker/rootfs".into()),
            ..Default::default()
        }
    }

    fn spec() -> Spec {
        Crun::spec(
            &cfg(),
            "holger-scan-abc",
            &[
                ("/w/j/artifact".into(), "/in/artifact".into()),
                ("/etc/holger/yara".into(), "/in/d-yara".into()),
            ],
            &[],
            true,
        )
        .unwrap()
    }

    #[test]
    fn the_bundle_has_every_hardening_line() {
        let c = oci_config(&spec());
        assert_eq!(c["root"]["readonly"], true);
        assert_eq!(c["root"]["path"], "/data/holger/sandbox/worker/rootfs");
        assert_eq!(c["process"]["noNewPrivileges"], true);
        assert_eq!(c["process"]["user"]["uid"], 65532);
        assert_eq!(c["process"]["user"]["gid"], 65532);
        assert_eq!(
            c["process"]["args"],
            serde_json::json!(["/holger-scan-worker", "/in"])
        );
        for set in [
            "bounding",
            "effective",
            "inheritable",
            "permitted",
            "ambient",
        ] {
            assert_eq!(
                c["process"]["capabilities"][set],
                serde_json::json!([]),
                "{set}"
            );
        }
        let ns: Vec<&str> = c["linux"]["namespaces"]
            .as_array()
            .unwrap()
            .iter()
            .map(|n| n["type"].as_str().unwrap())
            .collect();
        for want in ["pid", "mount", "network", "uts", "user", "ipc"] {
            assert!(ns.contains(&want), "namespace {want} in {ns:?}");
        }
        assert!(
            c["linux"]["namespaces"]
                .as_array()
                .unwrap()
                .iter()
                .all(|n| n.get("path").is_none()),
            "every namespace is NEW, none joined"
        );
        assert_eq!(c["linux"]["uidMappings"][0]["hostID"], DEFAULT_USERNS_BASE);
        assert_eq!(c["linux"]["uidMappings"][0]["size"], USERNS_SIZE);
        assert_eq!(c["linux"]["gidMappings"], c["linux"]["uidMappings"]);
        let r = &c["linux"]["resources"];
        assert_eq!(r["memory"]["limit"], 1024 << 20);
        assert_eq!(
            r["memory"]["swap"],
            1024 << 20,
            "no swap: the swap limit equals the memory limit"
        );
        assert_eq!(r["cpu"]["quota"], 100_000);
        assert_eq!(r["cpu"]["period"], 100_000);
        assert_eq!(r["pids"]["limit"], 64);
        assert_eq!(c["linux"]["cgroupsPath"], "/holger-sandbox/holger-scan-abc");
        assert_eq!(c["linux"]["seccomp"]["defaultAction"], "SCMP_ACT_ERRNO");
        let mounts = c["mounts"].as_array().unwrap();
        let scratch = mounts
            .iter()
            .find(|m| m["destination"] == "/scratch")
            .expect("the scratch tmpfs");
        for o in ["size=256m", "mode=1777", "noexec", "nosuid", "nodev"] {
            assert!(
                scratch["options"]
                    .as_array()
                    .unwrap()
                    .iter()
                    .any(|x| x == o),
                "{o} in {scratch}"
            );
        }
        for (dest, host) in [
            ("/in/artifact", "/w/j/artifact"),
            ("/in/d-yara", "/etc/holger/yara"),
        ] {
            let m = mounts
                .iter()
                .find(|m| m["destination"] == dest)
                .unwrap_or_else(|| panic!("{dest}"));
            assert_eq!(m["source"], host);
            let o = m["options"].as_array().unwrap();
            assert!(
                o.iter().any(|x| x == "ro") && !o.iter().any(|x| x == "rw"),
                "{dest} is read-only: {m}"
            );
            assert!(
                o.iter().any(|x| x == "noexec")
                    && o.iter().any(|x| x == "nosuid")
                    && o.iter().any(|x| x == "nodev"),
                "{m}"
            );
        }
        assert!(!c.to_string().contains("unconfined"));
        assert!(c["linux"]["maskedPaths"]
            .as_array()
            .unwrap()
            .iter()
            .any(|p| p == "/proc/kcore"));
    }

    #[test]
    fn a_jobs_output_is_its_one_writable_bind() {
        let args = crate::run_args_out(
            &cfg(),
            "holger-scan-abc",
            Path::new("/w/j/artifact"),
            Path::new("/w/j/job.json"),
            &[crate::Mount::new("/w/in", "i")],
            Some(Path::new("/w/j/out")),
        );
        let c = oci_config(&Crun::spec_for(&cfg(), "holger-scan-abc", &args, true).unwrap());
        let mounts = c["mounts"].as_array().unwrap();
        let out = mounts
            .iter()
            .find(|m| m["destination"] == "/out")
            .expect("the job's /out");
        assert_eq!(out["source"], "/w/j/out");
        let o = out["options"].as_array().unwrap();
        assert!(
            o.iter().any(|x| x == "rw")
                && o.iter().any(|x| x == "noexec")
                && o.iter().any(|x| x == "nosuid"),
            "{out}"
        );
        let input = mounts
            .iter()
            .find(|m| m["destination"] == "/in/i")
            .expect("the job's input");
        assert!(
            input["options"]
                .as_array()
                .unwrap()
                .iter()
                .any(|x| x == "ro"),
            "{input}"
        );
        let binds_rw = mounts
            .iter()
            .filter(|m| {
                m["type"] == "bind" && m["options"].as_array().unwrap().iter().any(|x| x == "rw")
            })
            .count();
        assert_eq!(binds_rw, 1, "one writable bind: {c}");
        // Without an output directory the spec is the scan's, unchanged.
        let plain = crate::run_args(
            &cfg(),
            "n",
            Path::new("/w/j/artifact"),
            Path::new("/w/j/job.json"),
            &[],
        );
        assert!(
            !oci_config(&Crun::spec_for(&cfg(), "n", &plain, true).unwrap())["mounts"]
                .as_array()
                .unwrap()
                .iter()
                .any(|m| m["destination"] == "/out")
        );
    }

    #[test]
    fn a_command_replaces_the_default_argument_and_ipc_is_optional() {
        let s = Crun::spec(
            &cfg(),
            "n",
            &[],
            &["probe".into(), "--sleep".into(), "3".into()],
            false,
        )
        .unwrap();
        assert_eq!(s.argv, ["/holger-scan-worker", "probe", "--sleep", "3"]);
        let c = oci_config(&s);
        assert!(!c["linux"]["namespaces"]
            .as_array()
            .unwrap()
            .iter()
            .any(|n| n["type"] == "ipc"));
    }

    #[test]
    fn clamds_generated_profile_is_the_pinned_one_and_crun_adds_only_its_own_calls() {
        use sha2::Digest as _;
        let got: String = sha2::Sha256::digest(CLAMD_SECCOMP_JSON.as_bytes())
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect();
        let got = format!("sha256:{got}");
        assert_eq!(got, CLAMD_SECCOMP_SHA256);
        let p = clamd_seccomp_profile();
        assert_eq!(p["defaultAction"], "SCMP_ACT_KILL_PROCESS");
        let calls = p["syscalls"].as_array().unwrap();
        let added = calls.last().unwrap();
        assert_eq!(added["names"], serde_json::json!(RUNTIME_AFTER_FILTER));
        let names: Vec<&str> = calls[..calls.len() - 1]
            .iter()
            .flat_map(|c| {
                c["names"]
                    .as_array()
                    .unwrap()
                    .iter()
                    .map(|n| n.as_str().unwrap())
            })
            .collect();
        for never in [
            "execve", "execveat", "ptrace", "mount", "unshare", "setns", "connect",
        ] {
            assert!(
                !names.contains(&never),
                "clamd's measured profile names {never}"
            );
        }
        // clamd's one socket is its unix listener: socket(AF_UNIX, SOCK_STREAM, 0) only.
        let socket: Vec<_> = calls.iter().filter(|c| c["names"][0] == "socket").collect();
        assert_eq!(socket.len(), 1);
        assert_eq!(socket[0]["args"][0]["valueTwo"], 1, "AF_UNIX");
    }

    #[test]
    fn the_seccomp_profile_is_the_capability_free_reduction_of_the_default() {
        let p = seccomp_profile();
        assert_eq!(p["defaultAction"], "SCMP_ACT_ERRNO");
        assert_eq!(
            p["defaultErrnoRet"], 38,
            "ENOSYS for what is not listed, as podman"
        );
        let arches: Vec<&str> = p["architectures"]
            .as_array()
            .unwrap()
            .iter()
            .map(|a| a.as_str().unwrap())
            .collect();
        assert_eq!(
            arches,
            ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"]
        );
        let names = |action: &str| -> Vec<String> {
            p["syscalls"]
                .as_array()
                .unwrap()
                .iter()
                .filter(|s| s["action"] == action)
                .flat_map(|s| {
                    s["names"]
                        .as_array()
                        .unwrap()
                        .iter()
                        .map(|n| n.as_str().unwrap().to_string())
                })
                .collect()
        };
        let allowed = names("SCMP_ACT_ALLOW");
        for want in [
            "read",
            "write",
            "openat",
            "mmap",
            "clone",
            "clone3",
            "futex",
            "exit_group",
            "arch_prctl",
            "prctl",
        ] {
            assert!(allowed.contains(&want.to_string()), "{want} is allowed");
        }
        for never in NEVER_WITHOUT_CAPABILITIES {
            assert!(
                !allowed.contains(&never.to_string()),
                "{never} is never allowed without a capability"
            );
        }
        assert!(
            allowed.contains(&"openat".to_string()) && allowed.len() > 300,
            "the ordinary syscalls stay: {}",
            allowed.len()
        );
        assert!(
            p.to_string().contains("\"args\""),
            "the argument-filtered rules (personality, socket) keep their filters"
        );
        for s in p["syscalls"].as_array().unwrap() {
            assert!(
                s.get("includes").is_none()
                    && s.get("excludes").is_none()
                    && s.get("comment").is_none(),
                "runtime-spec keys only: {s}"
            );
        }
    }

    #[test]
    fn a_config_without_a_rootfs_or_with_a_relative_one_is_refused_by_name() {
        let none = SandboxConfig {
            engine: crate::EngineKind::Crun,
            ..Default::default()
        };
        assert!(
            matches!(Crun::default().ready(&none), Err(Refusal::Config(w)) if w.contains("rootfs"))
        );
        let rel = SandboxConfig {
            engine: crate::EngineKind::Crun,
            rootfs: Some("worker/rootfs".into()),
            ..Default::default()
        };
        assert!(matches!(
            Crun::default().ready(&rel),
            Err(Refusal::Config(_))
        ));
        let dir = tempfile::tempdir().unwrap();
        let absent = SandboxConfig {
            engine: crate::EngineKind::Crun,
            rootfs: Some(dir.path().display().to_string()),
            ..Default::default()
        };
        assert!(
            matches!(Crun::default().ready(&absent), Err(Refusal::ImageMissing(w)) if w.contains("holger-scan-worker")),
            "{:?}",
            Crun::default().ready(&absent)
        );
    }

    #[test]
    fn a_missing_runtime_program_is_no_engine() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("holger-scan-worker"), b"\x7fELF").unwrap();
        let c = SandboxConfig {
            engine: crate::EngineKind::Crun,
            rootfs: Some(dir.path().display().to_string()),
            crun: "/nonexistent/holger-test/crun".into(),
            work_dir: Some(dir.path().display().to_string()),
            ..Default::default()
        };
        assert!(
            matches!(Crun::default().ready(&c), Err(Refusal::NoEngine(_))),
            "{:?}",
            Crun::default().ready(&c)
        );
    }

    #[test]
    fn the_cgroup_check_names_the_missing_controller() {
        let dir = tempfile::tempdir().unwrap();
        assert!(
            matches!(cgroup_ready(dir.path()), Err(Refusal::NoEngine(w)) if w.contains("not mounted"))
        );
        std::fs::write(
            dir.path().join("cgroup.controllers"),
            "cpuset cpu io memory\n",
        )
        .unwrap();
        assert!(
            matches!(cgroup_ready(dir.path()), Err(Refusal::NoEngine(w)) if w.contains("pids"))
        );
        std::fs::write(
            dir.path().join("cgroup.controllers"),
            "cpuset cpu io memory pids\n",
        )
        .unwrap();
        cgroup_ready(dir.path()).unwrap();
    }

    #[test]
    fn an_oom_kill_is_read_from_the_cgroup_events() {
        let dir = tempfile::tempdir().unwrap();
        let cg = dir.path().join(CGROUP_PARENT).join("x");
        std::fs::create_dir_all(&cg).unwrap();
        std::fs::write(
            cg.join("memory.events"),
            "low 0\nhigh 0\nmax 3\noom 1\noom_kill 1\n",
        )
        .unwrap();
        assert!(oom_killed_in(dir.path(), "x"));
        std::fs::write(cg.join("memory.events"), "low 0\noom_kill 0\n").unwrap();
        assert!(!oom_killed_in(dir.path(), "x"));
        assert!(!oom_killed_in(dir.path(), "y"));
    }

    #[test]
    fn the_bundle_is_written_under_the_state_and_removed_after() {
        let dir = tempfile::tempdir().unwrap();
        let st = State {
            dir: dir.path().to_path_buf(),
        };
        let b = st.write_bundle(&spec()).unwrap();
        let text = std::fs::read_to_string(b.join("config.json")).unwrap();
        assert!(text.contains("\"noNewPrivileges\": true"));
        st.remove_bundle("holger-scan-abc");
        assert!(!b.exists());
    }

    #[test]
    fn the_runtime_is_run_without_pivot_root_and_without_a_keyring() {
        let a = Runtime::run_args(
            "holger-scan-x",
            Path::new("/data/holger/sandbox/state/bundles/holger-scan-x"),
        );
        assert_eq!(a[0], "run");
        assert!(
            a.contains(&"--no-pivot".to_string()),
            "the appliance's root is the initramfs: pivot_root cannot leave it"
        );
        assert!(a.contains(&"--no-new-keyring".to_string()));
        assert_eq!(a.last().map(String::as_str), Some("holger-scan-x"));
        assert!(
            !a.iter().any(|x| x == "--detach"),
            "the runtime's stdout is the worker's: foreground"
        );
    }

    #[test]
    fn a_user_reads_as_numbers_only() {
        assert_eq!(parse_user("65532:65532").unwrap(), (65532, 65532));
        assert_eq!(parse_user("100:101").unwrap(), (100, 101));
        assert!(parse_user("nobody").is_err());
    }
}