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/lib.rs

//! **Holger's scan sandbox.** Every scan that unpacks, parses or runs an engine
//! over an artifact's bytes runs here, in a container, never in the
//! holger-server process (owner rule, 2026-10-23).
//!
//! One [`Sandbox::run`] is one container over one artifact:
//!
//! | What | How |
//! |---|---|
//! | No network | `--network none` |
//! | The artifact, read-only | bind-mounted at `/in/artifact`, `ro` |
//! | The job, read-only | `/in/job.json`, `ro`; extra inputs under `/in/m/<n>`, `ro` |
//! | A scratch space with a cap | `--tmpfs /scratch:rw,size=<n>m,mode=1777,noexec,nosuid,nodev` |
//! | CPU, memory, pids | `--cpus` (from `cpu_millis`), `--memory` (and `--memory-swap` the same: no swap), `--pids-limit` |
//! | A wall-clock timeout | the container is killed at the deadline; the scan is REFUSED ([`Refusal::Timeout`]) |
//! | A read-only rootfs | `--read-only` (and `--read-only-tmpfs=false`: no hidden writable /tmp) |
//! | No capabilities | `--cap-drop all` |
//! | No privilege gain | `--security-opt no-new-privileges` |
//! | The default seccomp | never `seccomp=unconfined`; the engine's default filter |
//! | A non-root user | `--user 65532:65532` |
//! | Rootless | the engine must report rootless ([`Refusal::NotRootless`]) |
//! | Never pulls | `--pull never`; a missing image is [`Refusal::ImageMissing`] |
//! | A job's output ([`Sandbox::run_job`]) | `/out`, fresh and empty per run, the ONE writable bind; only regular files are handed over, anything else refuses the run |
//! | Pinned by digest | a local tag is resolved ONCE to its digest ([`Engine::pin`], [`pinned_ref`]) and every run names `<repository>@sha256:…`, never the tag (NCSC P1, lane HOL-NCSC) |
//!
//! The worker answers with ONE document on stdout. This crate reads at most
//! [`Limits::report_max_bytes`] of it (more is [`Refusal::ReportTooLarge`] and
//! the container is killed) and hands the bytes back; the caller validates
//! them. Nothing else the worker writes reaches the server: the stderr tail is
//! kept for the refusal's words only.
//!
//! **Fail closed.** No engine is [`Refusal::NoEngine`], by name. There is no
//! in-process fallback anywhere in this crate.
//!
//! The engine is podman, the same one the Build Thing drives, two ways: the
//! local `podman` program ([`Podman`]), or podman's API socket ([`api::PodmanApi`],
//! `url`), the seam a Holger that itself runs in a container uses. Either way the
//! worker's stdout is ATTACHED, never read back from a log: measured in draupnir,
//! podman's compat log stream handed back a prefix of a container's output as if
//! it were the whole, and a scan verdict cut short must never look complete.

pub mod api;
pub mod crun;
pub mod media;
pub mod pids;
pub mod reap;
pub mod remote;

use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};

use holger_errcode::{self as ec, ErrCode};
use serde::{Deserialize, Serialize};

/// Holger's release, the version of the products (`holger`, `holger-downstream`,
/// `holger-cli`, `holger-scan-worker`, `holger-ui`) and of `holger-server-lib`. This crate
/// keeps its own semver line (4.0.1 in Holger 3.1.1), so the worker tag cannot be its own
/// version; holger-server-lib's tests hold the two equal (lane HOL-310-RELEASE).
macro_rules! release {
    () => {
        "3.1.1"
    };
}
pub const RELEASE: &str = release!();

/// The worker image a default config names: minted locally, never pulled, and tagged with
/// Holger's release (lane HOL-FIELD-TEST P1: one fixed tag let a stale worker answer a newer
/// server).
pub const DEFAULT_IMAGE: &str = concat!("localhost/holger-scan-worker:", release!());

/// Where the artifact is inside the container.
pub const ARTIFACT_PATH: &str = "/in/artifact";
/// Where the job is inside the container.
pub const JOB_PATH: &str = "/in/job.json";
/// The capped scratch space inside the container.
pub const SCRATCH_PATH: &str = "/scratch";
/// The directory the extra read-only inputs are mounted under (`/in/<name>`).
pub const EXTRA_DIR: &str = "/in";
/// A job's one writable directory (lane HOL-SANDBOX-TRAIT): fresh and empty per
/// run, its regular files handed to the caller after the run ([`Sandbox::run_job`]).
pub const OUT_PATH: &str = "/out";

/// The container's user: the distroless `nonroot` uid and gid.
pub const DEFAULT_USER: &str = "65532:65532";

/// How much of stderr is kept for a refusal's words.
const STDERR_TAIL: usize = 8 << 10;

// ── children of a PID 1 ─────────────────────────────────────────────────────

/// A claim on a child's exit status (`appliance_pid1::Claim` on the appliance):
/// held while the engine waits for its runtime process, so a PID 1's reaper
/// leaves the status to it. MEASURED on the appliance (lane HOLGER-MEDIA):
/// between two `try_wait` polls the reaper took the exit, the engine read
/// `ECHILD`, and a finished scan with no status is a refusal.
pub type ChildClaim = Box<dyn std::any::Any + Send>;

static CLAIM_CHILD: std::sync::OnceLock<fn(i32) -> ChildClaim> = std::sync::OnceLock::new();

/// Install the claim a PID 1 process offers; set once, by the appliance binary.
pub fn set_child_claim(f: fn(i32) -> ChildClaim) {
    let _ = CLAIM_CHILD.set(f);
}

/// Claim `child` for the caller, when a PID 1 offered claims.
pub fn claim_child(child: &Child) -> Option<ChildClaim> {
    CLAIM_CHILD.get().map(|f| f(child.id() as i32))
}

// ── the configuration ───────────────────────────────────────────────────────

/// The limits of one scan container. Each field has a default and can be set
/// alone: `limits: (timeout_secs: 30)`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Limits {
    /// Host CPU the worker may use, in thousandths of a core (1000 = one core).
    pub cpu_millis: u64,
    /// Memory, MiB. Swap is off (`--memory-swap` equals it).
    pub memory_mib: u64,
    /// Processes and threads.
    pub pids: u64,
    /// The tmpfs scratch space, MiB (it counts against `memory_mib`).
    pub scratch_mib: u64,
    /// Wall clock, seconds, from the start of the container to its report.
    pub timeout_secs: u64,
    /// The report's size cap, bytes.
    pub report_max_bytes: u64,
}

impl Limits {
    pub const DEFAULT_CPU_MILLIS: u64 = 1000;
    pub const DEFAULT_MEMORY_MIB: u64 = 1024;
    pub const DEFAULT_PIDS: u64 = 64;
    pub const DEFAULT_SCRATCH_MIB: u64 = 256;
    pub const DEFAULT_TIMEOUT_SECS: u64 = 120;
    pub const DEFAULT_REPORT_MAX_BYTES: u64 = 8 << 20;
}

impl Default for Limits {
    fn default() -> Self {
        Self {
            cpu_millis: Self::DEFAULT_CPU_MILLIS,
            memory_mib: Self::DEFAULT_MEMORY_MIB,
            pids: Self::DEFAULT_PIDS,
            scratch_mib: Self::DEFAULT_SCRATCH_MIB,
            timeout_secs: Self::DEFAULT_TIMEOUT_SECS,
            report_max_bytes: Self::DEFAULT_REPORT_MAX_BYTES,
        }
    }
}

/// Which engine runs the container.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EngineKind {
    /// The `podman` program, or its API socket when `url` is set.
    #[default]
    Podman,
    /// The `crun` program over a rootfs directory; Holger writes the OCI bundle.
    Crun,
    /// A scan-worker SIDECAR over a unix socket ([`remote`]): the Kubernetes
    /// pod, which holds no container runtime. `work_dir` is the directory both
    /// containers share.
    Remote,
    /// Heimdall's protected runner (lane HOL-HEIMDALL-02, the `heimdall:` block): not an
    /// engine of this crate but the job runner holger-job-runner builds with its feature
    /// `heimdall`. Where Heimdall cannot start, `youki-plain`, then crun over `rootfs`.
    Heimdall,
}

/// The `sandbox:` block: which engine, which image, the limits.
///
/// ```ron
/// sandbox: Some((
///     image: "localhost/holger-scan-worker:3.1.1",
///     limits: (memory_mib: 2048, timeout_secs: 300),
/// )),
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SandboxConfig {
    /// Which engine runs the container: the `podman` program or its API
    /// socket ([`EngineKind::Podman`], the default), or `crun` over a rootfs
    /// directory with Holger writing the bundle ([`EngineKind::Crun`], the
    /// appliance's engine; [`crun`]).
    pub engine: EngineKind,
    /// The podman program.
    pub podman: String,
    /// The OCI runtime program (`engine: crun`).
    pub crun: String,
    /// The worker's root directory (`engine: crun`): the image laid on disk,
    /// verified before the server serves. Absolute.
    pub rootfs: Option<String>,
    /// Where the bundles and the runtime's state are kept (`engine: crun`).
    /// `None`: under `work_dir`, else the system temporary directory.
    pub state_dir: Option<String>,
    /// The first host uid a container's uid 0 maps to (`engine: crun`); host
    /// 0 is never in the map. Default 100000.
    pub userns_base: u32,
    /// The sidecar's unix socket (`engine: remote`). `None`:
    /// [`remote::DEFAULT_SOCKET`].
    pub socket: Option<String>,
    /// podman's API socket (`unix:///run/user/1000/podman/podman.sock`): the
    /// engine is driven over its REST API ([`api::PodmanApi`]). `None`: the
    /// local `podman` program.
    pub url: Option<String>,
    /// The scan worker image. A local name (`localhost/…`) or one pinned by
    /// digest (`…@sha256:<64 hex>`); never a moving tag from a registry.
    pub image: String,
    pub limits: Limits,
    /// Where each scan's job directory is made (the artifact is written there
    /// and mounted read-only). It must be a path the engine sees as the server
    /// does. `None`: the system temporary directory.
    pub work_dir: Option<String>,
    /// The container's `uid:gid`. Never `0`.
    pub user: String,
    /// Refuse an engine that runs as root. Default true.
    pub require_rootless: bool,
    /// `engine: heimdall`: Heimdall's runner and the worker image it runs ([`HeimdallSettings`]).
    pub heimdall: Option<HeimdallSettings>,
}

/// **The `heimdall:` block** of the sandbox config (`engine: heimdall`, lane HOL-HEIMDALL-02).
/// holger-job-runner's `heimdall::start` reads it.
///
/// ```ron
/// sandbox: Some((
///     engine: heimdall,
///     rootfs: Some("/data/holger/sandbox/worker"),   // the crun fallback, when set
///     heimdall: Some((
///         image_layout: "/data/holger/sandbox/oci",
///         rootfs_tar: Some("/data/holger/sandbox/holger-scan-worker.rootfs.tar"),
///         shim: Some("/data/holger/sandbox/heimdall-container-shim"),
///     )),
/// )),
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct HeimdallSettings {
    /// The local OCI image layout the worker image is in (absolute).
    pub image_layout: String,
    /// The worker image's manifest digest (`sha256:<64 hex>`). `None`: minted at boot
    /// from `rootfs_tar` into `image_layout`.
    pub image: Option<String>,
    /// The worker's rootfs as one tar (the medium's verified
    /// `holger-scan-worker.rootfs.tar`), when `image` is `None`.
    pub rootfs_tar: Option<String>,
    /// The worker's path inside the image.
    pub worker: String,
    /// `heimdall-container-shim` (`None`: `HEIMDALL_CONTAINER_SHIM`, else next to the server).
    pub shim: Option<String>,
    /// `heimdall-plain-shim`, for the `youki-plain` fallback (`None`: `HEIMDALL_PLAIN_SHIM`,
    /// else next to the server; no shim there, no such fallback).
    pub plain_shim: Option<String>,
    /// Heimdall's strict mode: refuse to start without Landlock, cgroup v2 with
    /// memory/pids/cpu and user namespaces. Default true.
    pub strict: bool,
    /// The cgroup v2 mount and the jobs' parent cgroup under it.
    pub cgroup_root: String,
    pub cgroup_parent: String,
    /// Per-job bundles and the runtime's state (`None`: the sandbox's `state_dir`, else
    /// `work_dir`, else the system temporary directory).
    pub work_dir: Option<String>,
    /// When Heimdall refuses to start: run in `youki-plain` (when its shim is there).
    /// Default true. crun over `rootfs` is the next fallback.
    pub fallback_plain: bool,
    /// Heimdall's released verify-bundle guests (lane HOL-HEIMDALL-04): the directory the
    /// medium lays them in. Set, every bundle's verify-bundle job runs the split wasm guest
    /// (verifier + decoder, pinned) inside the worker; the server checks every file against
    /// its pin at boot.
    pub guests: Option<String>,
    /// The guests' only trust root (the korp root, PEM or DER), with `guests`.
    pub guests_trust_root: Option<String>,
    /// How many host ids a job's root range spans when this server runs as root under
    /// `require_rootless` (it starts at `userns_base`). Default 65536.
    pub host_id_count: u32,
    /// The scan worker's GENERATED seccomp profile (holger-job-runner `seccomp`, measured
    /// with heimdall-probe over the worker's real jobs): Heimdall installs it on top of its
    /// fixed profile for every job but the probe. Default true; false runs the jobs under
    /// the fixed profile only.
    pub generated_seccomp: bool,
}

/// The worker's path inside its image ([`media::worker_rootfs_tar`] lays it there).
pub const HEIMDALL_WORKER_BIN: &str = "/holger-scan-worker";

impl Default for HeimdallSettings {
    fn default() -> Self {
        Self {
            image_layout: String::new(),
            image: None,
            rootfs_tar: None,
            worker: HEIMDALL_WORKER_BIN.into(),
            shim: None,
            plain_shim: None,
            strict: true,
            cgroup_root: "/sys/fs/cgroup".into(),
            cgroup_parent: "heimdall".into(),
            work_dir: None,
            fallback_plain: true,
            guests: None,
            guests_trust_root: None,
            host_id_count: 65536,
            generated_seccomp: true,
        }
    }
}

impl HeimdallSettings {
    /// Refuse a block that cannot mean what it says, by name.
    pub fn validate(&self) -> Result<(), String> {
        let abs = |what: &str, p: &str| {
            if p.starts_with('/') {
                Ok(())
            } else {
                Err(format!("heimdall: {what} {p:?} is not an absolute path"))
            }
        };
        abs("image_layout", &self.image_layout)?;
        abs("worker", &self.worker)?;
        let pinned = |d: &str| {
            d.strip_prefix("sha256:").is_some_and(|h| {
                h.len() == 64 && h.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f'))
            })
        };
        match (&self.image, &self.rootfs_tar) {
            (Some(d), _) if !pinned(d) => {
                return Err(format!(
                    "heimdall: image {d:?} is not sha256:<64 lower-case hex>"
                ))
            }
            (None, None) => return Err(
                "heimdall: name the worker image (`image`, its digest) or the rootfs tar to mint it from (`rootfs_tar`)".into(),
            ),
            (None, Some(t)) => abs("rootfs_tar", t)?,
            _ => {}
        }
        if self.guests.is_some() != self.guests_trust_root.is_some() {
            return Err("heimdall: `guests` and `guests_trust_root` go together (the guests are verified against that root only)".into());
        }
        for (what, p) in [
            ("shim", &self.shim),
            ("plain_shim", &self.plain_shim),
            ("work_dir", &self.work_dir),
            ("guests", &self.guests),
            ("guests_trust_root", &self.guests_trust_root),
        ] {
            if let Some(p) = p {
                abs(what, p)?;
            }
        }
        abs("cgroup_root", &self.cgroup_root)
    }
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self {
            engine: EngineKind::Podman,
            podman: "podman".into(),
            crun: crun::DEFAULT_CRUN.into(),
            rootfs: None,
            state_dir: None,
            userns_base: crun::DEFAULT_USERNS_BASE,
            socket: None,
            url: None,
            image: DEFAULT_IMAGE.into(),
            limits: Limits::default(),
            work_dir: None,
            user: DEFAULT_USER.into(),
            require_rootless: true,
            heimdall: None,
        }
    }
}

impl SandboxConfig {
    /// `engine: heimdall` with `rootfs` set: the interim crun runner over it, Heimdall's
    /// last fallback (lane HOL-HEIMDALL-02). `None` for any other engine, or no `rootfs`.
    pub fn crun_fallback(&self) -> Option<SandboxConfig> {
        (self.engine == EngineKind::Heimdall && self.rootfs.is_some()).then(|| SandboxConfig {
            engine: EngineKind::Crun,
            heimdall: None,
            ..self.clone()
        })
    }

    /// Refuse a config that cannot mean what it says, by name.
    pub fn validate(&self) -> Result<(), Refusal> {
        if self.engine == EngineKind::Heimdall {
            let Some(h) = &self.heimdall else {
                return Err(Refusal::Config(
                    "engine `heimdall` needs the `heimdall:` block: the worker image and the shim"
                        .into(),
                ));
            };
            h.validate().map_err(Refusal::Config)?;
            if let Some(f) = self.crun_fallback() {
                f.validate()?;
            }
            return Ok(());
        }
        if self.engine == EngineKind::Crun {
            match self.rootfs.as_deref() {
                None => {
                    return Err(Refusal::Config(
                        "engine `crun` needs `rootfs`: the worker's root directory".into(),
                    ))
                }
                Some(r) if !r.starts_with('/') => {
                    return Err(Refusal::Config(format!(
                        "rootfs {r:?} is not an absolute path"
                    )))
                }
                Some(_) => {}
            }
            if self.require_rootless && self.userns_base == 0 {
                return Err(Refusal::Config("userns_base 0 would map the container's root to the host's; require_rootless refuses it".into()));
            }
        } else if self.engine == EngineKind::Remote {
            // The sidecar's image is the pod's to pin; the server needs the
            // shared directory and the socket.
            match self.work_dir.as_deref() {
                None => return Err(Refusal::Config(
                    "engine `remote` needs `work_dir`: the directory shared with the scan sidecar"
                        .into(),
                )),
                Some(d) if !d.starts_with('/') => {
                    return Err(Refusal::Config(format!(
                        "work_dir {d:?} is not an absolute path"
                    )))
                }
                Some(_) => {}
            }
            if let Some(s) = self.socket.as_deref().filter(|s| !s.starts_with('/')) {
                return Err(Refusal::Config(format!(
                    "socket {s:?} is not an absolute path"
                )));
            }
        } else if !image_is_pinned(&self.image) {
            return Err(Refusal::Config(format!(
                "image {:?} is neither a local name (localhost/…) nor pinned by digest (@sha256:…); \
                 the scan worker is minted, never pulled",
                self.image
            )));
        }
        let uid = self.user.split(':').next().unwrap_or("");
        if uid.is_empty()
            || uid == "0"
            || uid == "root"
            || !self.user.chars().all(|c| c.is_ascii_digit() || c == ':')
        {
            return Err(Refusal::Config(format!(
                "user {:?} must be a numeric non-root uid:gid",
                self.user
            )));
        }
        let l = &self.limits;
        if l.cpu_millis == 0
            || l.memory_mib < 64
            || l.pids < 8
            || l.scratch_mib == 0
            || l.timeout_secs == 0
            || l.report_max_bytes < 1024
        {
            return Err(Refusal::Config(format!("limits {l:?} are below the floor (cpu_millis > 0, memory_mib >= 64, pids >= 8, scratch_mib > 0, timeout_secs > 0, report_max_bytes >= 1024)")));
        }
        if l.scratch_mib >= l.memory_mib {
            return Err(Refusal::Config(format!(
                "scratch_mib {} must be below memory_mib {}: the tmpfs is charged to the container's memory",
                l.scratch_mib, l.memory_mib
            )));
        }
        Ok(())
    }
}

/// A local name, or a digest pin.
pub fn image_is_pinned(image: &str) -> bool {
    if image.starts_with("localhost/") && !image.contains("..") {
        return true;
    }
    match image.rsplit_once("@sha256:") {
        Some((name, hex)) => {
            !name.is_empty() && hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit())
        }
        None => false,
    }
}

/// **`image` pinned by `digest`** (`sha256:<64 hex>`, what the engine says the image is now):
/// `<repository>@<digest>`, the tag dropped. An image the config already pins by digest stands
/// as written. No digest, or one that is not a sha256, is [`Refusal::ImageMissing`]: a scan runs
/// only an image named by its digest.
pub fn pinned_ref(image: &str, digest: &str) -> Result<String, Refusal> {
    let sha256 = |hex: &str| hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit());
    if image
        .rsplit_once("@sha256:")
        .is_some_and(|(name, hex)| !name.is_empty() && sha256(hex))
    {
        return Ok(image.to_string());
    }
    let digest = digest.trim();
    if !digest.strip_prefix("sha256:").is_some_and(sha256) {
        return Err(Refusal::ImageMissing(format!(
            "{image} (the engine names no sha256 digest for it, {digest:?}; a scan runs only an image named by its digest)"
        )));
    }
    // The tag is a `:` in the last path segment; a `:` before the last `/` is a registry port.
    let repository = match image.rsplit_once('/') {
        Some((head, last)) => format!("{head}/{}", last.split_once(':').map_or(last, |(n, _)| n)),
        None => image.split_once(':').map_or(image, |(n, _)| n).to_string(),
    };
    Ok(format!("{repository}@{}", digest.to_ascii_lowercase()))
}

// ── the refusals ────────────────────────────────────────────────────────────

/// Why a scan did not produce a report. Every one is a REFUSAL of the artifact:
/// a scan that did not finish never passes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
    /// No container engine answers: holger refuses to scan.
    NoEngine(String),
    /// The worker ran past its wall clock and was killed.
    Timeout { secs: u64 },
    /// The container was killed by a limit (memory, pids) or crashed.
    Killed {
        exit: Option<i32>,
        oom: bool,
        stderr: String,
    },
    /// The worker wrote more than the report cap.
    ReportTooLarge { cap: u64 },
    /// The report does not read as the schema says (the caller's validation).
    ReportInvalid(String),
    /// `sandbox: off` in a release build.
    OffInRelease,
    /// The engine runs as root.
    NotRootless,
    /// The worker image is not on this host; it is never pulled.
    ImageMissing(String),
    /// The worker exited non-zero without a report.
    WorkerFailed { exit: Option<i32>, stderr: String },
    /// The sandbox config cannot mean what it says.
    Config(String),
    /// The job directory could not be written.
    Io(String),
    /// clamd in its sandbox is not answering (lane HOLGER-MEDIA).
    ClamdDown(String),
    /// The scan sidecar has nothing bounding a job's processes, in a release build (RA5-27).
    NoPidsLimit(String),
    /// The laid sandbox (a rootfs or the runtime) differs from the medium's manifest, and laying
    /// it again from the medium did not make it equal (lane HOL-NCSC): the scans stay refused.
    RootfsDrift(String),
    /// A real build talks to ClamAV only over the sandboxed clamd's unix socket (lane HOL-NCSC):
    /// no TCP clamd, no host clamd, no `clamscan` (refused at the start, by name).
    ClamdNotSandboxed(String),
}

impl Refusal {
    /// The frozen `holger-sandbox-<n>` code.
    pub fn code(&self) -> &'static ErrCode {
        match self {
            Refusal::NoEngine(_) => &ec::SANDBOX_NO_ENGINE,
            Refusal::Timeout { .. } => &ec::SANDBOX_TIMEOUT,
            Refusal::Killed { .. } => &ec::SANDBOX_KILLED,
            Refusal::ReportTooLarge { .. } => &ec::SANDBOX_REPORT_TOO_LARGE,
            Refusal::ReportInvalid(_) => &ec::SANDBOX_REPORT_INVALID,
            Refusal::OffInRelease => &ec::SANDBOX_OFF_IN_RELEASE,
            Refusal::NotRootless => &ec::SANDBOX_NOT_ROOTLESS,
            Refusal::ImageMissing(_) => &ec::SANDBOX_IMAGE_MISSING,
            Refusal::WorkerFailed { .. } => &ec::SANDBOX_WORKER_FAILED,
            Refusal::Config(_) => &ec::SANDBOX_CONFIG,
            Refusal::Io(_) => &ec::SANDBOX_IO,
            Refusal::ClamdDown(_) => &ec::SANDBOX_CLAMD_DOWN,
            Refusal::NoPidsLimit(_) => &ec::SANDBOX_NO_PIDS_LIMIT,
            Refusal::RootfsDrift(_) => &ec::SANDBOX_ROOTFS_DRIFT,
            Refusal::ClamdNotSandboxed(_) => &ec::SANDBOX_CLAMD_NOT_SANDBOXED,
        }
    }

    /// The short name a finding id carries (`sandbox-timeout`).
    pub fn name(&self) -> &'static str {
        match self {
            Refusal::NoEngine(_) => "sandbox-no-engine",
            Refusal::Timeout { .. } => "sandbox-timeout",
            Refusal::Killed { .. } => "sandbox-killed",
            Refusal::ReportTooLarge { .. } => "sandbox-report-too-large",
            Refusal::ReportInvalid(_) => "sandbox-report-invalid",
            Refusal::OffInRelease => "sandbox-off-in-release",
            Refusal::NotRootless => "sandbox-not-rootless",
            Refusal::ImageMissing(_) => "sandbox-image-missing",
            Refusal::WorkerFailed { .. } => "sandbox-worker-failed",
            Refusal::Config(_) => "sandbox-config",
            Refusal::Io(_) => "sandbox-io",
            Refusal::ClamdDown(_) => "sandbox-clamd-down",
            Refusal::NoPidsLimit(_) => "sandbox-no-pids-limit",
            Refusal::RootfsDrift(_) => "sandbox-rootfs-drift",
            Refusal::ClamdNotSandboxed(_) => "sandbox-clamd-not-sandboxed",
        }
    }

    fn words(&self) -> String {
        match self {
            Refusal::NoEngine(why) => format!("no container engine to scan in, so holger refuses to scan: {why}"),
            Refusal::Timeout { secs } => format!("the scan worker ran past its {secs} s wall clock and was killed"),
            Refusal::Killed { exit, oom, stderr } => format!(
                "the scan worker was killed by its limits (exit {exit:?}{}){}",
                if *oom { ", out of memory" } else { "" },
                tail_words(stderr)
            ),
            Refusal::ReportTooLarge { cap } => format!("the scan worker wrote more than the {cap}-byte report cap"),
            Refusal::ReportInvalid(why) => format!("the scan worker's report is not valid: {why}"),
            Refusal::OffInRelease => "`sandbox: off` is allowed only in a test build; this is a release build".into(),
            Refusal::NotRootless => "the container engine runs as root; the scan sandbox needs rootless podman".into(),
            Refusal::ImageMissing(img) => format!("the scan worker image {img} is not on this host (it is minted and carried, never pulled)"),
            Refusal::WorkerFailed { exit, stderr } => {
                format!("the scan worker exited {exit:?} without a report{}", tail_words(stderr))
            }
            Refusal::Config(why) => format!("the sandbox config is not valid: {why}"),
            Refusal::Io(why) => format!("the scan job could not be prepared: {why}"),
            Refusal::ClamdDown(why) => format!("clamd in its sandbox is not answering, so holger refuses to scan: {why}"),
            Refusal::NoPidsLimit(why) => format!("nothing bounds a scan job's processes, so holger refuses to scan: {why}"),
            Refusal::RootfsDrift(why) => format!("the laid sandbox differs from the medium, so holger refuses to scan: {why}"),
            Refusal::ClamdNotSandboxed(why) => format!("a real build talks to ClamAV only over the sandboxed clamd's unix socket: {why}"),
        }
    }
}

fn tail_words(stderr: &str) -> String {
    let t = stderr.trim();
    if t.is_empty() {
        String::new()
    } else {
        let last: String = t
            .lines()
            .rev()
            .take(3)
            .collect::<Vec<_>>()
            .into_iter()
            .rev()
            .collect::<Vec<_>>()
            .join(" | ");
        format!(": {}", last.chars().take(400).collect::<String>())
    }
}

impl std::fmt::Display for Refusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.code().tag(self.words()))
    }
}

impl std::error::Error for Refusal {}

// ── one run ─────────────────────────────────────────────────────────────────

/// One extra read-only input (`/in/<name>`): a YARA rules directory, the job's
/// own files (provenance material, a trust root).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mount {
    pub host: PathBuf,
    /// One path segment: `[A-Za-z0-9._-]+`.
    pub name: String,
}

impl Mount {
    pub fn new(host: impl Into<PathBuf>, name: impl Into<String>) -> Self {
        Self {
            host: host.into(),
            name: name.into(),
        }
    }

    /// The path inside the container.
    pub fn container_path(&self) -> String {
        format!("{EXTRA_DIR}/{}", self.name)
    }
}

/// What one scan runs on.
#[derive(Debug, Clone)]
pub struct Request<'a> {
    /// The artifact's bytes. Written to the job directory and mounted read-only.
    pub artifact: &'a [u8],
    /// The job document the worker reads at [`JOB_PATH`].
    pub job: &'a [u8],
    pub mounts: Vec<Mount>,
    /// The artifact's sha256 hex, for the container's name.
    pub sha256: &'a str,
    /// The worker's arguments; empty (always, for a scan) runs the image's own
    /// entrypoint. The live tests pass `probe` to look at the box from inside.
    pub command: Vec<String>,
}

/// What came back from a finished worker.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output {
    /// The report bytes, at most [`Limits::report_max_bytes`].
    pub report: Vec<u8>,
    pub elapsed: Duration,
}

/// The engine seam: one container run. [`Podman`] is the real one; a test
/// substitutes its own.
pub trait Engine: Send + Sync {
    /// Is the engine there, rootless, with the image? `Ok` or the refusal.
    fn ready(&self, cfg: &SandboxConfig) -> Result<(), Refusal>;
    /// Run `argv` (the `run` arguments, [`run_args`]) and return the finished
    /// worker's stdout, read under the cap and the deadline.
    fn run(&self, cfg: &SandboxConfig, name: &str, args: &[String]) -> Result<Output, Refusal>;
    /// **The image this engine runs, pinned by digest** (NCSC P1, lane HOL-NCSC): asked once,
    /// after [`Engine::ready`], and every run names what it answers. podman resolves a local
    /// tag to the digest it names now ([`pinned_ref`]), so a re-tag after the start cannot swap
    /// what runs. An engine that runs no image by name (crun over the laid rootfs, the sidecar,
    /// a test's) answers the configured name.
    fn pin(&self, cfg: &SandboxConfig) -> Result<String, Refusal> {
        Ok(cfg.image.clone())
    }
}

/// The sandbox of one server: its config and its engine.
pub struct Sandbox {
    cfg: SandboxConfig,
    engine: Box<dyn Engine>,
    /// The engine's first answer: the image pinned by digest, or the refusal.
    ready: std::sync::OnceLock<Result<String, Refusal>>,
}

impl std::fmt::Debug for Sandbox {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Sandbox").field("cfg", &self.cfg).finish()
    }
}

impl Sandbox {
    /// The sandbox over podman. The config is validated now; the engine is
    /// checked at the first scan (and then remembered).
    /// With `url`, podman's API socket ([`api::PodmanApi`]: a Holger in a
    /// container drives the host's rootless podman, no podman program beside
    /// it); without, the local `podman` program.
    pub fn podman(cfg: SandboxConfig) -> Result<Self, Refusal> {
        Self::from_config(cfg)
    }

    /// The sandbox the config declares: `engine: podman` (the program, or
    /// its socket with `url`) or `engine: crun` ([`crun::Crun`]).
    pub fn from_config(cfg: SandboxConfig) -> Result<Self, Refusal> {
        let engine: Box<dyn Engine> = match (cfg.engine, &cfg.url) {
            (EngineKind::Heimdall, _) => {
                return Err(Refusal::Config(
                    "engine `heimdall` is Heimdall's job runner (holger-job-runner, feature `heimdall`), not a container engine of the sandbox".into(),
                ))
            }
            (EngineKind::Crun, _) => Box::new(crun::Crun::default()),
            (EngineKind::Remote, _) => Box::new(remote::Remote::new(&cfg)),
            (EngineKind::Podman, Some(u)) => Box::new(api::PodmanApi::new(u)?),
            (EngineKind::Podman, None) => Box::new(Podman),
        };
        Self::with_engine(cfg, engine)
    }

    /// Which engine this sandbox runs, for the boot log.
    pub fn engine_words(&self) -> &'static str {
        match (self.cfg.engine, &self.cfg.url) {
            (EngineKind::Crun, _) => {
                "crun over the laid worker rootfs (a user namespace, no network)"
            }
            (EngineKind::Remote, _) => {
                "the scan-worker sidecar over its unix socket (no runtime in this pod, no network)"
            }
            (EngineKind::Podman, Some(_)) => "rootless podman over its API socket, no network",
            (EngineKind::Podman, None) => "rootless podman, no network",
            (EngineKind::Heimdall, _) => "Heimdall's protected runner",
        }
    }

    pub fn with_engine(cfg: SandboxConfig, engine: Box<dyn Engine>) -> Result<Self, Refusal> {
        cfg.validate()?;
        Ok(Self {
            cfg,
            engine,
            ready: std::sync::OnceLock::new(),
        })
    }

    pub fn config(&self) -> &SandboxConfig {
        &self.cfg
    }

    /// The image every run names, pinned by digest, once the engine answered.
    pub fn pinned_image(&self) -> Option<String> {
        self.ready.get().and_then(|r| r.as_ref().ok().cloned())
    }

    /// **Ready, and the image pinned**: the engine is asked once, then its digest of the
    /// configured image ([`Engine::pin`]); both are remembered for the server's life, so a
    /// re-tag on the host after the start changes nothing that runs. A sidecar is asked every
    /// time and names no image.
    fn pinned(&self) -> Result<String, Refusal> {
        if self.cfg.engine == EngineKind::Remote {
            self.engine.ready(&self.cfg)?;
            return Ok(self.cfg.image.clone());
        }
        self.ready
            .get_or_init(|| {
                self.engine.ready(&self.cfg)?;
                self.engine.pin(&self.cfg)
            })
            .clone()
    }

    /// Is the engine ready? Asked once, then remembered: a refusal stays a
    /// refusal until the server restarts (fail closed, and cheap). A sidecar
    /// (`engine: remote`) is asked every time: it starts beside the server, may
    /// come later and may go, and a ping over a local socket is cheap.
    pub fn ready(&self) -> Result<(), Refusal> {
        self.pinned().map(|_| ())
    }

    /// Run the worker over one artifact, the image named by its digest.
    pub fn run(&self, req: &Request<'_>) -> Result<Output, Refusal> {
        let image = self.pinned()?;
        let cfg = SandboxConfig {
            image,
            ..self.cfg.clone()
        };
        check_mounts(&req.mounts)?;
        let dir = self.job_dir()?;
        let (artifact, job) = write_inputs(dir.path(), req)?;
        let name = container_name(req.sha256);
        let mut args = run_args(&cfg, &name, &artifact, &job, &req.mounts);
        args.extend(req.command.iter().cloned());
        self.engine.run(&cfg, &name, &args)
    }

    /// **Run one job with an output directory** (lane HOL-SANDBOX-TRAIT): as
    /// [`Self::run`], under `limits` instead of the config's, with one fresh, empty,
    /// writable directory mounted at [`OUT_PATH`]. After a finished run every
    /// regular file the worker wrote there is moved into `out` (which must exist and
    /// be empty); anything else there (a link, a directory) refuses the run and
    /// nothing is handed over.
    pub fn run_job(
        &self,
        req: &Request<'_>,
        out: &Path,
        limits: &Limits,
    ) -> Result<Output, Refusal> {
        SandboxConfig {
            limits: limits.clone(),
            ..self.cfg.clone()
        }
        .validate()?;
        let mut there = std::fs::read_dir(out).map_err(|e| {
            Refusal::Config(format!("the job's output directory {}: {e}", out.display()))
        })?;
        if there.next().is_some() {
            return Err(Refusal::Config(format!(
                "the job's output directory {} is not empty",
                out.display()
            )));
        }
        // The image named by its digest, as every run (lane HOL-NCSC).
        let image = self.pinned()?;
        let cfg = SandboxConfig {
            image,
            limits: limits.clone(),
            ..self.cfg.clone()
        };
        check_mounts(&req.mounts)?;
        let dir = self.job_dir()?;
        let (artifact, job) = write_inputs(dir.path(), req)?;
        // The worker's one writable place: fresh, empty, writable by its (mapped) user.
        let job_out = dir.path().join("out");
        std::fs::create_dir(&job_out)
            .map_err(|e| Refusal::Io(format!("{}: {e}", job_out.display())))?;
        set_mode(&job_out, 0o777)?;
        let name = container_name(req.sha256);
        let mut args = run_args_out(&cfg, &name, &artifact, &job, &req.mounts, Some(&job_out));
        args.extend(req.command.iter().cloned());
        let o = self.engine.run(&cfg, &name, &args)?;
        hand_over(&job_out, out)?;
        Ok(o)
    }

    fn job_dir(&self) -> Result<tempfile::TempDir, Refusal> {
        let b = match &self.cfg.work_dir {
            Some(d) => {
                std::fs::create_dir_all(d).map_err(|e| Refusal::Io(format!("{d}: {e}")))?;
                tempfile::Builder::new()
                    .prefix("holger-scan-")
                    .tempdir_in(d)
            }
            None => tempfile::Builder::new().prefix("holger-scan-").tempdir(),
        };
        let dir = b.map_err(|e| Refusal::Io(format!("the job directory: {e}")))?;
        set_mode(dir.path(), 0o755)?;
        Ok(dir)
    }
}

/// Every extra input is one path segment under [`EXTRA_DIR`], from an absolute host path.
fn check_mounts(mounts: &[Mount]) -> Result<(), Refusal> {
    for m in mounts {
        if !valid_segment(&m.name)
            || m.name == "artifact"
            || m.name == "job.json"
            || m.name == "out"
        {
            return Err(Refusal::Config(format!(
                "mount name {:?} is not one path segment",
                m.name
            )));
        }
        if !m.host.is_absolute() {
            return Err(Refusal::Config(format!(
                "mount {} is not an absolute path",
                m.host.display()
            )));
        }
    }
    Ok(())
}

/// The artifact and the job document, written read-only into the job directory.
fn write_inputs(dir: &Path, req: &Request<'_>) -> Result<(PathBuf, PathBuf), Refusal> {
    let artifact = dir.join("artifact");
    let job = dir.join("job.json");
    write_ro(&artifact, req.artifact)?;
    write_ro(&job, req.job)?;
    Ok((artifact, job))
}

/// Move every regular file the worker wrote in `from` into `to`. Anything else
/// there refuses the run before a single file is moved.
fn hand_over(from: &Path, to: &Path) -> Result<(), Refusal> {
    let mut files = Vec::new();
    for e in std::fs::read_dir(from).map_err(|e| Refusal::Io(format!("{}: {e}", from.display())))? {
        let e = e.map_err(|e| Refusal::Io(format!("{}: {e}", from.display())))?;
        let name = e.file_name().to_string_lossy().into_owned();
        let ft = e
            .file_type()
            .map_err(|e| Refusal::Io(format!("{name}: {e}")))?;
        if !ft.is_file() || !valid_segment(&name) {
            return Err(Refusal::ReportInvalid(format!(
                "the job wrote {name:?} into its output directory, which is not a plain regular file; nothing is handed over"
            )));
        }
        files.push(name);
    }
    for n in files {
        let (a, b) = (from.join(&n), to.join(&n));
        if std::fs::rename(&a, &b).is_err() {
            // Another file system: copy, then remove the job's copy.
            std::fs::copy(&a, &b).map_err(|e| Refusal::Io(format!("handing over {n}: {e}")))?;
            let _ = std::fs::remove_file(&a);
        }
    }
    Ok(())
}

fn valid_segment(s: &str) -> bool {
    !s.is_empty()
        && s != "."
        && s != ".."
        && s.bytes()
            .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
}

fn write_ro(path: &Path, bytes: &[u8]) -> Result<(), Refusal> {
    std::fs::write(path, bytes).map_err(|e| Refusal::Io(format!("{}: {e}", path.display())))?;
    // Readable by the container's non-root user (a rootless engine maps it to
    // a subordinate uid), writable by nobody.
    set_mode(path, 0o444)
}

fn set_mode(path: &Path, mode: u32) -> Result<(), Refusal> {
    use std::os::unix::fs::PermissionsExt as _;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
        .map_err(|e| Refusal::Io(format!("{}: {e}", path.display())))
}

/// `holger-scan-<sha8>-<nonce>`: a name only this server's scans use, so a
/// cleanup removes only its own container.
pub fn container_name(sha256: &str) -> String {
    let short: String = sha256
        .chars()
        .filter(|c| c.is_ascii_hexdigit())
        .take(12)
        .collect();
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.subsec_nanos())
        .unwrap_or(0);
    let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    format!(
        "holger-scan-{short}-{}-{seq}-{nanos:08x}",
        std::process::id()
    )
}

static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

/// **The container, as podman `run` arguments.** Pure, so every hardening flag
/// is tested without an engine. The worker's argv is the image's own.
pub fn run_args(
    cfg: &SandboxConfig,
    name: &str,
    artifact: &Path,
    job: &Path,
    mounts: &[Mount],
) -> Vec<String> {
    let l = &cfg.limits;
    let mut a: Vec<String> = Vec::new();
    let mut push = |s: String| a.push(s);
    push("run".into());
    push(format!("--name={name}"));
    push("--pull=never".into());
    push("--network=none".into());
    push("--read-only".into());
    push("--read-only-tmpfs=false".into());
    push("--cap-drop=all".into());
    push("--security-opt=no-new-privileges".into());
    push(format!("--user={}", cfg.user));
    push(format!(
        "--cpus={}.{:03}",
        l.cpu_millis / 1000,
        l.cpu_millis % 1000
    ));
    push(format!("--memory={}m", l.memory_mib));
    push(format!("--memory-swap={}m", l.memory_mib));
    push(format!("--pids-limit={}", l.pids));
    push(format!(
        "--tmpfs={SCRATCH_PATH}:rw,size={}m,mode=1777,noexec,nosuid,nodev",
        l.scratch_mib
    ));
    push(format!(
        "--volume={}:{ARTIFACT_PATH}:ro",
        artifact.display()
    ));
    push(format!("--volume={}:{JOB_PATH}:ro", job.display()));
    for m in mounts {
        push(format!(
            "--volume={}:{}:ro",
            m.host.display(),
            m.container_path()
        ));
    }
    push("--env=HOME=/scratch".into());
    push("--env=TMPDIR=/scratch".into());
    push("--log-driver=none".into());
    push("--stop-timeout=1".into());
    push("--init=false".into());
    push(cfg.image.clone());
    a
}

/// [`run_args`] with a job's writable output directory at [`OUT_PATH`].
pub fn run_args_out(
    cfg: &SandboxConfig,
    name: &str,
    artifact: &Path,
    job: &Path,
    mounts: &[Mount],
    out: Option<&Path>,
) -> Vec<String> {
    let mut a = run_args(cfg, name, artifact, job, mounts);
    if let Some(o) = out {
        // Before the image: everything after it is the worker's argv.
        let at = a.len() - 1;
        a.insert(at, format!("--volume={}:{OUT_PATH}:rw", o.display()));
    }
    a
}

/// The writable output directory of a `run` argument list (the host path mounted
/// at [`OUT_PATH`]), when the run has one.
pub fn out_of(args: &[String]) -> Option<String> {
    args.iter()
        .filter_map(|a| a.strip_prefix("--volume="))
        .filter_map(|v| v.strip_suffix(&format!(":{OUT_PATH}:rw")))
        .map(String::from)
        .next()
}

/// The read-only mounts of a `run` argument list, `(host, container)`: what
/// an engine that does not run the podman program decodes from [`run_args`].
pub(crate) fn mounts_of(args: &[String]) -> Vec<(String, String)> {
    args.iter()
        .filter_map(|a| a.strip_prefix("--volume="))
        .filter_map(|v| v.strip_suffix(":ro"))
        .filter_map(|v| v.rsplit_once(':'))
        .map(|(h, c)| (h.to_string(), c.to_string()))
        .collect()
}

/// The worker's own argv of a `run` argument list: everything after the image.
pub(crate) fn command_of(cfg: &SandboxConfig, args: &[String]) -> Vec<String> {
    match args.iter().position(|a| a == &cfg.image) {
        Some(i) => args[i + 1..].to_vec(),
        None => Vec::new(),
    }
}

// ── podman ──────────────────────────────────────────────────────────────────

/// The real engine: the podman program.
#[derive(Debug, Clone, Copy, Default)]
pub struct Podman;

impl Podman {
    fn cmd(cfg: &SandboxConfig) -> Command {
        // A32-14, a process by design: the podman engine, the sandbox's configured container program; the sandbox is the feature.
        let mut c = Command::new(&cfg.podman);
        c.stdin(Stdio::null());
        c
    }

    fn quiet(cfg: &SandboxConfig, args: &[&str]) -> Result<std::process::Output, Refusal> {
        Self::cmd(cfg)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .output()
            .map_err(|e| Refusal::NoEngine(format!("{} {}: {e}", cfg.podman, args.join(" "))))
    }

    /// Remove our own container (by its name), whatever state it is in.
    fn remove(cfg: &SandboxConfig, name: &str) {
        let _ = Self::quiet(cfg, &["rm", "--force", "--time=0", "--ignore", name]);
    }

    fn oom_killed(cfg: &SandboxConfig, name: &str) -> bool {
        Self::quiet(
            cfg,
            &[
                "inspect",
                "--type=container",
                "--format={{.State.OOMKilled}}",
                name,
            ],
        )
        .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
        .unwrap_or(false)
    }
}

impl Engine for Podman {
    fn ready(&self, cfg: &SandboxConfig) -> Result<(), Refusal> {
        let info = Self::quiet(cfg, &["info", "--format={{.Host.Security.Rootless}}"])?;
        if !info.status.success() {
            return Err(Refusal::NoEngine(format!(
                "`{} info` failed: {}",
                cfg.podman,
                String::from_utf8_lossy(&info.stderr).trim()
            )));
        }
        if cfg.require_rootless && String::from_utf8_lossy(&info.stdout).trim() != "true" {
            return Err(Refusal::NotRootless);
        }
        let img = Self::quiet(cfg, &["image", "exists", &cfg.image])?;
        if !img.status.success() {
            return Err(Refusal::ImageMissing(cfg.image.clone()));
        }
        Ok(())
    }

    fn pin(&self, cfg: &SandboxConfig) -> Result<String, Refusal> {
        let o = Self::quiet(
            cfg,
            &["image", "inspect", "--format={{.Digest}}", &cfg.image],
        )?;
        if !o.status.success() {
            return Err(Refusal::ImageMissing(cfg.image.clone()));
        }
        pinned_ref(&cfg.image, String::from_utf8_lossy(&o.stdout).trim())
    }

    fn run(&self, cfg: &SandboxConfig, name: &str, args: &[String]) -> Result<Output, Refusal> {
        let started = Instant::now();
        let child = Self::cmd(cfg)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            // A32-14, a process by design: the podman process (see its constructor).
            .spawn()
            .map_err(|e| Refusal::NoEngine(format!("{}: {e}", cfg.podman)))?;
        let _claim = 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 if code == Some(137)
                    || code == Some(139)
                    || code.is_none()
                    || Self::oom_killed(cfg, name)
                {
                    Err(Refusal::Killed {
                        exit: code,
                        oom: Self::oom_killed(cfg, name),
                        stderr,
                    })
                } else {
                    Err(Refusal::WorkerFailed { exit: code, stderr })
                }
            }
            Drive::TimedOut => Err(Refusal::Timeout {
                secs: cfg.limits.timeout_secs,
            }),
            Drive::TooLarge => Err(Refusal::ReportTooLarge {
                cap: cfg.limits.report_max_bytes,
            }),
            Drive::Io(e) => Err(Refusal::Io(e)),
        };
        Self::remove(cfg, name);
        out
    }
}

pub(crate) enum Drive {
    Done {
        status: std::process::ExitStatus,
        stdout: Vec<u8>,
        stderr: String,
    },
    TimedOut,
    TooLarge,
    Io(String),
}

/// Read stdout (capped) and the stderr tail from one child, without a second
/// thread, until it exits, the deadline passes or stdout passes the cap. On the
/// deadline or the cap the podman client is killed; the caller then removes
/// the container by name, which kills the worker inside it.
pub(crate) fn drive(mut child: Child, deadline: Instant, cap: u64) -> Drive {
    let mut out = child.stdout.take();
    let mut err = child.stderr.take();
    for p in [
        out.as_ref().map(|p| p as &dyn std::os::fd::AsFd),
        err.as_ref().map(|p| p as &dyn std::os::fd::AsFd),
    ]
    .into_iter()
    .flatten()
    {
        if let Err(e) = nonblocking(p) {
            let _ = child.kill();
            let _ = child.wait();
            return Drive::Io(format!("the worker's pipes: {e}"));
        }
    }
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let mut buf = vec![0u8; 64 << 10];
    loop {
        let mut progressed = false;
        if let Some(p) = out.as_mut() {
            match p.read(&mut buf) {
                Ok(0) => out = None,
                Ok(n) => {
                    progressed = true;
                    if (stdout.len() + n) as u64 > cap {
                        let _ = child.kill();
                        let _ = child.wait();
                        return Drive::TooLarge;
                    }
                    stdout.extend_from_slice(&buf[..n]);
                }
                Err(e)
                    if e.kind() == std::io::ErrorKind::WouldBlock
                        || e.kind() == std::io::ErrorKind::Interrupted => {}
                Err(_) => out = None,
            }
        }
        if let Some(p) = err.as_mut() {
            match p.read(&mut buf) {
                Ok(0) => err = None,
                Ok(n) => {
                    progressed = true;
                    stderr.extend_from_slice(&buf[..n]);
                    if stderr.len() > 2 * STDERR_TAIL {
                        stderr.drain(..stderr.len() - STDERR_TAIL);
                    }
                }
                Err(e)
                    if e.kind() == std::io::ErrorKind::WouldBlock
                        || e.kind() == std::io::ErrorKind::Interrupted => {}
                Err(_) => err = None,
            }
        }
        if out.is_none() && err.is_none() {
            match child.try_wait() {
                Ok(Some(status)) => {
                    return Drive::Done {
                        status,
                        stdout,
                        stderr: String::from_utf8_lossy(&stderr).into_owned(),
                    }
                }
                Ok(None) => {}
                Err(e) => return Drive::Io(format!("waiting for the worker: {e}")),
            }
        }
        if Instant::now() >= deadline {
            let _ = child.kill();
            let _ = child.wait();
            return Drive::TimedOut;
        }
        if !progressed {
            std::thread::sleep(Duration::from_millis(5));
        }
    }
}

fn nonblocking(fd: &dyn std::os::fd::AsFd) -> std::io::Result<()> {
    use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
    let fl = fcntl_getfl(fd)?;
    fcntl_setfl(fd, fl | OFlags::NONBLOCK)?;
    Ok(())
}

#[cfg(test)]
mod worker_tag_tests {
    /// **The worker image is tagged with the server's version** (lane HOL-FIELD-TEST P1): one
    /// fixed tag (`0.1.0`) across every server version let a stale worker answer a newer server.
    #[test]
    fn the_default_worker_image_names_this_version() {
        assert_eq!(
            super::DEFAULT_IMAGE,
            format!("localhost/holger-scan-worker:{}", super::RELEASE)
        );
    }
}

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

    fn args() -> Vec<String> {
        run_args(
            &SandboxConfig::default(),
            "holger-scan-abc",
            Path::new("/w/j/artifact"),
            Path::new("/w/j/job.json"),
            &[Mount::new("/etc/holger/yara", "d-yara")],
        )
    }

    #[test]
    fn the_container_has_every_hardening_flag() {
        let a = args();
        for want in [
            "--network=none",
            "--read-only",
            "--read-only-tmpfs=false",
            "--cap-drop=all",
            "--security-opt=no-new-privileges",
            "--user=65532:65532",
            "--pull=never",
            "--cpus=1.000",
            "--memory=1024m",
            "--memory-swap=1024m",
            "--pids-limit=64",
            "--tmpfs=/scratch:rw,size=256m,mode=1777,noexec,nosuid,nodev",
            "--volume=/w/j/artifact:/in/artifact:ro",
            "--volume=/w/j/job.json:/in/job.json:ro",
            "--volume=/etc/holger/yara:/in/d-yara:ro",
        ] {
            assert!(a.iter().any(|x| x == want), "missing {want} in {a:?}");
        }
        assert!(
            !a.iter().any(|x| x.contains("unconfined")),
            "the default seccomp stays: {a:?}"
        );
        assert!(
            !a.iter()
                .any(|x| x.contains("--privileged") || x.contains("--cap-add")),
            "{a:?}"
        );
        assert!(
            a.iter()
                .filter(|x| x.starts_with("--volume="))
                .all(|x| x.ends_with(":ro")),
            "every mount is read-only: {a:?}"
        );
        assert_eq!(
            a.last().map(String::as_str),
            Some(DEFAULT_IMAGE),
            "the image is last; its argv is its own"
        );
    }

    #[test]
    fn a_moving_tag_root_or_a_tiny_limit_is_refused_by_name() {
        let bad = SandboxConfig {
            image: "docker.io/clamav/clamav:latest".into(),
            ..Default::default()
        };
        assert_eq!(bad.validate().unwrap_err().code().code, "holger-sandbox-10");
        let pinned = SandboxConfig {
            image: format!("docker.io/clamav/clamav:1.5.4@sha256:{}", "e1".repeat(32)),
            ..Default::default()
        };
        pinned.validate().unwrap();
        let root = SandboxConfig {
            user: "0:0".into(),
            ..Default::default()
        };
        assert!(root.validate().is_err());
        let tiny = SandboxConfig {
            limits: Limits {
                scratch_mib: 2048,
                ..Default::default()
            },
            ..Default::default()
        };
        assert!(
            tiny.validate().is_err(),
            "the tmpfs is charged to the memory limit"
        );
    }

    #[test]
    fn every_limit_reads_alone_from_ron() {
        let c: SandboxConfig = ron::from_str("(limits: (timeout_secs: 5))").unwrap();
        assert_eq!(c.limits.timeout_secs, 5);
        assert_eq!(c.limits.memory_mib, Limits::DEFAULT_MEMORY_MIB);
        assert_eq!(c.image, DEFAULT_IMAGE);
        assert!(
            ron::from_str::<SandboxConfig>("(limit: ())").is_err(),
            "a misspelt key is refused"
        );
    }

    #[test]
    fn the_crun_engine_reads_from_ron_and_needs_an_absolute_rootfs() {
        let c: SandboxConfig = ron::from_str("(engine: crun, rootfs: Some(\"/data/holger/sandbox/worker/rootfs\"), crun: \"/data/holger/sandbox/crun\")").unwrap();
        assert_eq!(c.engine, EngineKind::Crun);
        c.validate().unwrap();
        assert_eq!(c.userns_base, crun::DEFAULT_USERNS_BASE);
        let none: SandboxConfig = ron::from_str("(engine: crun)").unwrap();
        assert_eq!(
            none.validate().unwrap_err().code().code,
            "holger-sandbox-10"
        );
        let root: SandboxConfig =
            ron::from_str("(engine: crun, rootfs: Some(\"/r\"), userns_base: 0)").unwrap();
        assert!(
            root.validate()
                .unwrap_err()
                .to_string()
                .contains("userns_base"),
            "host root is never mapped in"
        );
        let s = Sandbox::from_config(c).unwrap();
        assert!(s.engine_words().starts_with("crun"));
    }

    #[test]
    fn every_refusal_carries_its_code_and_says_refuses() {
        let r = Refusal::NoEngine("podman: not found".into());
        assert!(r.to_string().starts_with("[holger-sandbox-1] "), "{r}");
        assert!(r.to_string().contains("refuses to scan"), "{r}");
        assert_eq!(
            Refusal::Timeout { secs: 3 }.to_string(),
            "[holger-sandbox-2] the scan worker ran past its 3 s wall clock and was killed"
        );
    }

    #[test]
    fn a_local_tag_is_pinned_to_its_digest() {
        let d = format!("sha256:{}", "ab".repeat(32));
        assert_eq!(
            pinned_ref("localhost/holger-scan-worker:0.1.0", &d).unwrap(),
            format!("localhost/holger-scan-worker@{d}")
        );
        assert_eq!(
            pinned_ref("localhost:5000/w:1", &d).unwrap(),
            format!("localhost:5000/w@{d}"),
            "a registry port is not a tag"
        );
        assert_eq!(
            pinned_ref("localhost/w", &d).unwrap(),
            format!("localhost/w@{d}")
        );
        let already = format!("docker.io/x/y:1@{d}");
        assert_eq!(
            pinned_ref(&already, &format!("sha256:{}", "cd".repeat(32))).unwrap(),
            already,
            "a pin in the config stands"
        );
        let zz = format!("sha256:{}", "zz".repeat(32));
        for bad in ["", "sha256:", "sha256:abc", "md5:00", zz.as_str()] {
            let e = pinned_ref("localhost/w:1", bad).unwrap_err();
            assert_eq!(e.code().code, "holger-sandbox-8", "{bad:?}: {e}");
        }
    }

    /// What each run named: (the image, the run arguments).
    type Runs = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<String>)>>>;

    /// An engine that pins by digest and records what each run named.
    struct Pinning(Runs);
    impl Engine for Pinning {
        fn ready(&self, _: &SandboxConfig) -> Result<(), Refusal> {
            Ok(())
        }
        fn pin(&self, cfg: &SandboxConfig) -> Result<String, Refusal> {
            pinned_ref(&cfg.image, &format!("sha256:{}", "ab".repeat(32)))
        }
        fn run(&self, cfg: &SandboxConfig, _: &str, args: &[String]) -> Result<Output, Refusal> {
            self.0
                .lock()
                .unwrap()
                .push((cfg.image.clone(), args.to_vec()));
            Ok(Output {
                report: b"{}".to_vec(),
                elapsed: Duration::ZERO,
            })
        }
    }

    /// NCSC P1 (lane HOL-NCSC): the configured tag is resolved once, and every run names the
    /// image by that digest, never by the tag.
    #[test]
    fn every_run_names_the_image_by_its_digest_never_its_tag() {
        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
        let s =
            Sandbox::with_engine(SandboxConfig::default(), Box::new(Pinning(log.clone()))).unwrap();
        for _ in 0..2 {
            s.run(&Request {
                artifact: b"x",
                job: b"{}",
                mounts: vec![],
                sha256: "ab",
                command: vec![],
            })
            .unwrap();
        }
        let want = format!("localhost/holger-scan-worker@sha256:{}", "ab".repeat(32));
        let seen = log.lock().unwrap();
        assert_eq!(seen.len(), 2);
        for (image, args) in seen.iter() {
            assert_eq!(image, &want);
            assert!(args.iter().any(|a| a == &want), "{args:?}");
            assert!(
                !args.iter().any(|a| a == DEFAULT_IMAGE),
                "never the tag: {args:?}"
            );
        }
        assert_eq!(s.pinned_image().as_deref(), Some(want.as_str()));
    }

    struct Absent;
    impl Engine for Absent {
        fn ready(&self, _: &SandboxConfig) -> Result<(), Refusal> {
            Err(Refusal::NoEngine("no podman on this host".into()))
        }
        fn run(&self, _: &SandboxConfig, _: &str, _: &[String]) -> Result<Output, Refusal> {
            panic!("an absent engine is never run")
        }
    }

    #[test]
    fn no_engine_fails_closed_before_anything_runs() {
        let s = Sandbox::with_engine(SandboxConfig::default(), Box::new(Absent)).unwrap();
        let e = s
            .run(&Request {
                artifact: b"x",
                job: b"{}",
                mounts: vec![],
                sha256: "ab",
                command: vec![],
            })
            .unwrap_err();
        assert_eq!(e.code().code, "holger-sandbox-1");
    }

    #[test]
    fn the_real_podman_program_missing_is_no_engine() {
        let cfg = SandboxConfig {
            podman: "/nonexistent/holger-test/podman".into(),
            ..Default::default()
        };
        assert!(matches!(Podman.ready(&cfg), Err(Refusal::NoEngine(_))));
    }

    /// An engine that plays the worker: it writes `files` (name, bytes, or a link
    /// when the bytes are `LINK`) into the run's output directory and records the
    /// limits it was run under.
    struct Writes {
        files: Vec<(&'static str, &'static [u8])>,
        seen: std::sync::Arc<std::sync::Mutex<Option<Limits>>>,
    }
    const LINK: &[u8] = b"<link>";
    impl Engine for Writes {
        fn ready(&self, _: &SandboxConfig) -> Result<(), Refusal> {
            Ok(())
        }
        fn run(&self, cfg: &SandboxConfig, _: &str, args: &[String]) -> Result<Output, Refusal> {
            *self.seen.lock().unwrap() = Some(cfg.limits.clone());
            let out = PathBuf::from(out_of(args).expect("a job run has an output directory"));
            for (n, b) in &self.files {
                if *b == LINK {
                    std::os::unix::fs::symlink("/etc/passwd", out.join(n)).unwrap();
                } else {
                    std::fs::write(out.join(n), b).unwrap();
                }
            }
            Ok(Output {
                report: b"{}".to_vec(),
                elapsed: Duration::ZERO,
            })
        }
    }

    type Seen = std::sync::Arc<std::sync::Mutex<Option<Limits>>>;

    fn writes(files: Vec<(&'static str, &'static [u8])>) -> (Sandbox, Seen) {
        let seen = Seen::default();
        let s = Sandbox::with_engine(
            SandboxConfig::default(),
            Box::new(Writes {
                files,
                seen: seen.clone(),
            }),
        )
        .unwrap();
        (s, seen)
    }

    fn job_req() -> Request<'static> {
        Request {
            artifact: b"",
            job: b"{}",
            mounts: vec![],
            sha256: "ab",
            command: vec![],
        }
    }

    #[test]
    fn a_job_has_one_writable_out_and_every_other_mount_read_only() {
        let a = run_args_out(
            &SandboxConfig::default(),
            "holger-scan-abc",
            Path::new("/w/j/artifact"),
            Path::new("/w/j/job.json"),
            &[Mount::new("/w/in", "i")],
            Some(Path::new("/w/j/out")),
        );
        assert!(a.iter().any(|x| x == "--volume=/w/j/out:/out:rw"), "{a:?}");
        assert_eq!(
            a.iter().filter(|x| x.ends_with(":rw")).count(),
            1,
            "one writable mount: {a:?}"
        );
        assert_eq!(out_of(&a).as_deref(), Some("/w/j/out"));
        assert!(
            !mounts_of(&a).iter().any(|(_, c)| c == OUT_PATH),
            "the output is no read-only input"
        );
        assert!(
            a.iter().any(|x| x == "--network=none") && a.iter().any(|x| x == "--read-only"),
            "{a:?}"
        );
        assert_eq!(a.last().map(String::as_str), Some(DEFAULT_IMAGE));
        assert_eq!(
            run_args_out(
                &SandboxConfig::default(),
                "n",
                Path::new("/a"),
                Path::new("/j"),
                &[],
                None
            ),
            run_args(
                &SandboxConfig::default(),
                "n",
                Path::new("/a"),
                Path::new("/j"),
                &[]
            )
        );
        assert_eq!(out_of(&args()), None);
    }

    #[test]
    fn a_jobs_files_are_handed_over_under_its_own_limits() {
        let (s, seen) = writes(vec![("a", b"alpha"), ("members.json", b"{}")]);
        let dest = tempfile::tempdir().unwrap();
        let limits = Limits {
            timeout_secs: 7,
            memory_mib: 300,
            ..Limits::default()
        };
        s.run_job(&job_req(), dest.path(), &limits).unwrap();
        assert_eq!(std::fs::read(dest.path().join("a")).unwrap(), b"alpha");
        assert_eq!(
            std::fs::read(dest.path().join("members.json")).unwrap(),
            b"{}"
        );
        let seen = seen.lock().unwrap().clone().expect("the engine ran");
        assert_eq!(seen.timeout_secs, 7);
        assert_eq!(seen.memory_mib, 300);
    }

    #[test]
    fn a_linked_output_refuses_the_run_and_hands_over_nothing() {
        let (s, _) = writes(vec![("a", b"alpha"), ("passwd", LINK)]);
        let dest = tempfile::tempdir().unwrap();
        let e = s
            .run_job(&job_req(), dest.path(), &Limits::default())
            .unwrap_err();
        assert!(
            matches!(&e, Refusal::ReportInvalid(w) if w.contains("passwd")),
            "{e}"
        );
        assert_eq!(
            std::fs::read_dir(dest.path()).unwrap().count(),
            0,
            "nothing is handed over"
        );
    }

    #[test]
    fn a_job_needs_an_empty_output_directory_and_limits_above_the_floor() {
        let (s, _) = writes(vec![]);
        let dest = tempfile::tempdir().unwrap();
        std::fs::write(dest.path().join("old"), b"x").unwrap();
        assert!(matches!(
            s.run_job(&job_req(), dest.path(), &Limits::default())
                .unwrap_err(),
            Refusal::Config(_)
        ));
        let empty = tempfile::tempdir().unwrap();
        let tiny = Limits {
            memory_mib: 1,
            ..Limits::default()
        };
        assert!(matches!(
            s.run_job(&job_req(), empty.path(), &tiny).unwrap_err(),
            Refusal::Config(_)
        ));
    }

    #[test]
    fn a_child_past_its_deadline_is_killed_and_timed_out() {
        let child = Command::new("sleep")
            .arg("30")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        let t = Instant::now();
        assert!(matches!(
            drive(child, Instant::now() + Duration::from_millis(200), 1024),
            Drive::TimedOut
        ));
        assert!(t.elapsed() < Duration::from_secs(5));
    }

    #[test]
    fn a_child_past_the_cap_is_killed() {
        let child = Command::new("yes")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        assert!(matches!(
            drive(child, Instant::now() + Duration::from_secs(10), 4096),
            Drive::TooLarge
        ));
    }

    #[test]
    fn a_finished_child_hands_back_all_of_its_stdout() {
        let child = Command::new("sh")
            .args(["-c", "head -c 300000 /dev/zero; echo oops >&2"])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        match drive(child, Instant::now() + Duration::from_secs(10), 1 << 20) {
            Drive::Done {
                status,
                stdout,
                stderr,
            } => {
                assert!(status.success());
                assert_eq!(stdout.len(), 300_000);
                assert_eq!(stderr.trim(), "oops");
            }
            _ => panic!("not done"),
        }
    }
}