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

//! **The sandbox on the medium** (lane HOLGER-MEDIA, 2026-10-23): what a main or
//! twin box's app archive carries beside the server, and how it is laid on the
//! box before the server serves.
//!
//! The medium carries, under the entry directory [`ENTRY_DIR`] of the app
//! `.znippy` (the same signed archive korp-start verifies at every boot, so the
//! installer, the metered installer, the keep medium and the live app all carry
//! it):
//!
//! | Entry | What |
//! |---|---|
//! | [`MANIFEST`] | this manifest: every entry's sha256 and size |
//! | [`CRUN`] | the static OCI runtime (`crun-<ver>-linux-amd64-disable-systemd`) |
//! | [`WORKER_ROOTFS`] | the scan worker's root filesystem, a tar: the static worker, `/etc/passwd`, `/etc/group`, the mount points |
//! | [`CLAMAV_ROOTFS`] | the pinned ClamAV image flattened to one tar, its signature database inside |
//!
//! At boot the server lays each entry under its sandbox directory
//! (`<data_root>/sandbox`), checks its sha256 against the manifest (a second leg
//! over the archive's own signed root), unpacks the rootfs tars with every
//! owner shifted into the containers' user namespace, and only then builds the
//! scan worker ([`crate::crun::Crun`]) and starts clamd. An entry that is
//! missing, short or wrong is a refusal by name, and the scans stay refused
//! (`holger-sandbox-1`): never an unverified rootfs.

use std::io::Read as _;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::Refusal;

/// The entry directory inside the app archive.
pub const ENTRY_DIR: &str = "sandbox";
/// The manifest entry.
pub const MANIFEST: &str = "manifest.toml";
/// The OCI runtime entry.
pub const CRUN: &str = "crun";
/// The scan worker's rootfs entry.
pub const WORKER_ROOTFS: &str = "holger-scan-worker.rootfs.tar";
/// The ClamAV rootfs entry.
pub const CLAMAV_ROOTFS: &str = "clamav.rootfs.tar";
/// The manifest's schema.
pub const SCHEMA: &str = "holger.sandbox-media/v1";
/// Heimdall's shim (lane HOL-HEIMDALL-03): `heimdall-container-shim`, static, the program
/// `engine: heimdall` starts per job. A medium that carries it also lays the worker's rootfs
/// tar whole ([`Layout::worker_rootfs_tar`]): Heimdall mints the pinned worker image from it.
pub const HEIMDALL_SHIM: &str = "heimdall-container-shim";

/// The pinned static crun release.
pub const CRUN_VERSION: &str = "1.21";
pub const CRUN_URL: &str = "https://github.com/containers/crun/releases/download/1.21/crun-1.21-linux-amd64-disable-systemd";
pub const CRUN_SHA256: &str = "53843205fe3fe38fb8f2ef08798d55d4746d1172696d580ef1e6d7ef7ce3afe6";
/// The pinned ClamAV image (its signature database inside; `scripts/clamd-sandbox.sh` pins the same).
pub const CLAMAV_IMAGE: &str = "docker.io/clamav/clamav:1.5.4@sha256:e19130cdebaeacc71fa46eab3ed6f69941f2f22aa5e08d55e9197f8edf43ce5e";

/// `sandbox/<name>`: the entry's path inside the archive.
pub fn entry_path(name: &str) -> String {
    format!("{ENTRY_DIR}/{name}")
}

/// What one entry is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
    /// A static program, laid executable.
    Program,
    /// A root filesystem tar, unpacked into a directory.
    Rootfs,
    /// One of Heimdall's released verify-bundle guests (a module, its `.wasm.blake3`, its
    /// `.wasm.p7s`, the release's SLSA statement): laid read-only under
    /// [`Layout::heimdall_guests`] (lane HOL-HEIMDALL-03).
    Guest,
}

/// One entry of the manifest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
    /// The entry's file name under [`ENTRY_DIR`].
    pub name: String,
    pub kind: Kind,
    /// Its sha256, hex.
    pub sha256: String,
    pub bytes: u64,
    /// The image it was flattened from (a rootfs), or the program's version.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
}

/// The manifest: every entry the medium carries.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
    pub schema: String,
    #[serde(rename = "entry")]
    pub entries: Vec<Entry>,
}

impl Manifest {
    /// The manifest of the three entries, from their bytes.
    pub fn of(crun: &[u8], worker_rootfs: &[u8], clamav_rootfs: &[u8]) -> Manifest {
        Manifest {
            schema: SCHEMA.into(),
            entries: vec![
                Entry {
                    name: CRUN.into(),
                    kind: Kind::Program,
                    sha256: sha256_hex(crun),
                    bytes: crun.len() as u64,
                    source: Some(format!("crun {CRUN_VERSION}")),
                },
                Entry {
                    name: WORKER_ROOTFS.into(),
                    kind: Kind::Rootfs,
                    sha256: sha256_hex(worker_rootfs),
                    bytes: worker_rootfs.len() as u64,
                    source: Some(crate::DEFAULT_IMAGE.into()),
                },
                Entry {
                    name: CLAMAV_ROOTFS.into(),
                    kind: Kind::Rootfs,
                    sha256: sha256_hex(clamav_rootfs),
                    bytes: clamav_rootfs.len() as u64,
                    source: Some(CLAMAV_IMAGE.into()),
                },
            ],
        }
    }

    /// This manifest with one more entry (Heimdall's shim, a guest file).
    #[must_use]
    pub fn with(
        mut self,
        name: &str,
        kind: Kind,
        bytes: &[u8],
        source: Option<String>,
    ) -> Manifest {
        self.entries.push(Entry {
            name: name.into(),
            kind,
            sha256: sha256_hex(bytes),
            bytes: bytes.len() as u64,
            source,
        });
        self
    }

    /// Does this medium carry Heimdall (its shim)?
    pub fn carries_heimdall(&self) -> bool {
        self.entry(HEIMDALL_SHIM).is_some()
    }

    /// Refuse a manifest that is not what this build reads, by name.
    pub fn check(&self) -> Result<(), Refusal> {
        if self.schema != SCHEMA {
            return Err(Refusal::Config(format!(
                "the sandbox manifest schema is {:?}, this build reads {SCHEMA:?}",
                self.schema
            )));
        }
        for want in [CRUN, WORKER_ROOTFS, CLAMAV_ROOTFS] {
            if !self.entries.iter().any(|e| e.name == want) {
                return Err(Refusal::Config(format!(
                    "the sandbox manifest names no {want:?} entry"
                )));
            }
        }
        for e in &self.entries {
            if !valid_name(&e.name) {
                return Err(Refusal::Config(format!(
                    "the sandbox manifest names an entry {:?} that is not one plain file name",
                    e.name
                )));
            }
            if e.sha256.len() != 64 || !e.sha256.bytes().all(|b| b.is_ascii_hexdigit()) {
                return Err(Refusal::Config(format!(
                    "entry {:?}: sha256 {:?} is not 64 hex digits",
                    e.name, e.sha256
                )));
            }
            if e.bytes == 0 {
                return Err(Refusal::Config(format!("entry {:?} is empty", e.name)));
            }
        }
        Ok(())
    }

    pub fn entry(&self, name: &str) -> Option<&Entry> {
        self.entries.iter().find(|e| e.name == name)
    }

    /// Is `bytes` the entry `name` the manifest describes? The refusal names
    /// what differs.
    pub fn verify(&self, name: &str, bytes: &[u8]) -> Result<&Entry, Refusal> {
        let e = self.entry(name).ok_or_else(|| {
            Refusal::Config(format!("the sandbox manifest names no {name:?} entry"))
        })?;
        if bytes.len() as u64 != e.bytes {
            return Err(Refusal::ImageMissing(format!(
                "sandbox entry {name:?}: {} bytes on the medium, the manifest says {}",
                bytes.len(),
                e.bytes
            )));
        }
        let got = sha256_hex(bytes);
        if got != e.sha256 {
            return Err(Refusal::ImageMissing(format!(
                "sandbox entry {name:?}: sha256 {got} on the medium, the manifest says {}",
                e.sha256
            )));
        }
        Ok(e)
    }
}

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

pub fn sha256_hex(b: &[u8]) -> String {
    use sha2::Digest as _;
    sha2::Sha256::digest(b)
        .iter()
        .map(|x| format!("{x:02x}"))
        .collect()
}

// ── the worker's rootfs ─────────────────────────────────────────────────────

/// **The scan worker's root filesystem as a tar**, from its static binary: what
/// `distribution/scan-worker/Containerfile` says, as files. Every owner is
/// root (uid 0 inside the container's user namespace); [`lay_rootfs`] shifts
/// it onto the host.
pub fn worker_rootfs_tar(worker: &[u8]) -> Vec<u8> {
    let mut t = tar::Builder::new(Vec::new());
    t.mode(tar::HeaderMode::Deterministic);
    let dir = |t: &mut tar::Builder<Vec<u8>>, path: &str, mode: u32| {
        let mut h = tar::Header::new_gnu();
        h.set_entry_type(tar::EntryType::Directory);
        h.set_mode(mode);
        h.set_size(0);
        h.set_uid(0);
        h.set_gid(0);
        h.set_mtime(0);
        t.append_data(&mut h, path, std::io::empty())
            .expect("a tar in memory");
    };
    let file = |t: &mut tar::Builder<Vec<u8>>, path: &str, mode: u32, bytes: &[u8]| {
        let mut h = tar::Header::new_gnu();
        h.set_entry_type(tar::EntryType::Regular);
        h.set_mode(mode);
        h.set_size(bytes.len() as u64);
        h.set_uid(0);
        h.set_gid(0);
        h.set_mtime(0);
        t.append_data(&mut h, path, bytes).expect("a tar in memory");
    };
    file(&mut t, "holger-scan-worker", 0o555, worker);
    dir(&mut t, "etc", 0o755);
    file(
        &mut t,
        "etc/passwd",
        0o444,
        b"nonroot:x:65532:65532:nonroot:/scratch:/sbin/nologin\n",
    );
    file(&mut t, "etc/group", 0o444, b"nonroot:x:65532:\n");
    // A job's mount points (lane HOL-SANDBOX-TRAIT): the input `in/i`, the read paths
    // `in/r0`..`in/r3` and the one writable `out`; crun binds onto a read-only root, so a
    // target that is not laid here fails the container's start.
    for (d, m) in [
        ("scratch", 0o1777),
        ("in", 0o555),
        ("in/d-yara", 0o555),
        ("in/i", 0o555),
        ("in/r0", 0o555),
        ("in/r1", 0o555),
        ("in/r2", 0o555),
        ("in/r3", 0o555),
        ("out", 0o555),
        ("proc", 0o555),
        ("dev", 0o755),
        ("sys", 0o555),
        ("tmp", 0o1777),
    ] {
        dir(&mut t, d, m);
    }
    file(&mut t, "in/artifact", 0o444, b"");
    file(&mut t, "in/job.json", 0o444, b"");
    t.into_inner().expect("a tar in memory")
}

// ── laying on the box ───────────────────────────────────────────────────────

/// Where the sandbox lives on a box: `<data_root>/sandbox`.
#[derive(Debug, Clone)]
pub struct Layout {
    pub dir: PathBuf,
}

impl Layout {
    pub fn under(data_root: &Path) -> Layout {
        Layout {
            dir: data_root.join("sandbox"),
        }
    }
    /// The runtime program.
    pub fn crun(&self) -> PathBuf {
        self.dir.join(CRUN)
    }
    /// The worker's rootfs directory.
    pub fn worker_rootfs(&self) -> PathBuf {
        self.dir.join("worker").join("rootfs")
    }
    /// ClamAV's rootfs directory.
    pub fn clamav_rootfs(&self) -> PathBuf {
        self.dir.join("clamav").join("rootfs")
    }
    /// clamd's run directory (the socket appears in it) and its config.
    pub fn clamd_run(&self) -> PathBuf {
        self.dir.join("clamd")
    }
    /// The scanner signature database a signed `holger.scanner-sigs/v1` bundle installs
    /// (lane HOL-NCSC): `clamav/` is clamd's DatabaseDirectory once `installed.json` exists.
    /// Outside every rootfs, so the start's re-hash never sees it change.
    pub fn signatures(&self) -> PathBuf {
        self.dir.join("signatures")
    }
    pub fn clamd_socket(&self) -> PathBuf {
        self.clamd_run().join("clamd.sock")
    }
    /// The job directories (`work_dir`) and the runtime's state.
    pub fn jobs(&self) -> PathBuf {
        self.dir.join("jobs")
    }
    pub fn state(&self) -> PathBuf {
        self.dir.join("state")
    }
    /// Heimdall's shim (lane HOL-HEIMDALL-03), inside the exec view of the sandbox.
    pub fn heimdall_shim(&self) -> PathBuf {
        self.dir.join(HEIMDALL_SHIM)
    }
    /// The worker's rootfs tar, laid whole beside its unpacked tree when the medium carries
    /// Heimdall: `engine: heimdall` mints the worker image from it.
    pub fn worker_rootfs_tar(&self) -> PathBuf {
        self.dir.join(WORKER_ROOTFS)
    }
    /// The OCI layout Heimdall's worker image is minted into.
    pub fn heimdall_oci(&self) -> PathBuf {
        self.dir.join("oci")
    }
    /// Heimdall's per-job bundles and state.
    pub fn heimdall_work(&self) -> PathBuf {
        self.dir.join("heimdall")
    }
    /// Heimdall's released verify-bundle guests, read-only.
    pub fn heimdall_guests(&self) -> PathBuf {
        self.dir.join("heimdall-guests")
    }
    /// The record of what was laid: each entry's sha256, so a boot that finds
    /// the same medium lays nothing again.
    pub fn laid_record(&self) -> PathBuf {
        self.dir.join("laid.toml")
    }
}

/// **Unpack a rootfs tar into `dir`**, hardened: no absolute entry path, no `..`
/// in one, no device node or fifo, no setuid or setgid bit on a file, no hard
/// link outside the root, no relative symlink that climbs out of it, and
/// nothing ever written THROUGH a symlink (a `lib -> /x` followed by `lib/evil`
/// is refused by name). An ABSOLUTE symlink target (`bin/arch -> /bin/busybox`,
/// 306 of them in the ClamAV image) is allowed: it resolves inside the
/// container's own root and is never followed here. MEASURED on the first laid
/// box (2026-10-24): the stricter rule refused the whole ClamAV rootfs and every
/// scan with it. Every owner is shifted by `uid_shift` (the containers' user
/// namespace base) so the files are theirs inside and nobody's outside; a shift
/// of 0 leaves the owners as the unpacking user (a test, a developer's box).
/// The directory is replaced whole.
pub fn lay_rootfs(tar_bytes: &[u8], dir: &Path, uid_shift: u32) -> Result<usize, Refusal> {
    let io = |what: &str, e: std::io::Error| Refusal::Io(format!("{what}: {e}"));
    let staging = dir.with_extension("laying");
    let _ = remove_tree(&staging);
    std::fs::create_dir_all(&staging).map_err(|e| io(&staging.display().to_string(), e))?;
    let mut a = tar::Archive::new(tar_bytes);
    a.set_preserve_permissions(true);
    a.set_unpack_xattrs(false);
    a.set_overwrite(true);
    let mut n = 0usize;
    // Each directory's own mode is set LAST: laid as it comes, a read-only directory
    // (`in/` 0555 before `in/d-yara`) refuses the entries under it to an unpacking user
    // without CAP_DAC_OVERRIDE (every non-root run, measured by OCI-R1 with keep-id).
    let mut dirs: Vec<(PathBuf, u32)> = Vec::new();
    for entry in a.entries().map_err(|e| io("the rootfs tar", e))? {
        let mut entry = entry.map_err(|e| io("the rootfs tar", e))?;
        let path = entry
            .path()
            .map_err(|e| io("an entry's path", e))?
            .into_owned();
        let rel =
            checked_relative(&path).map_err(|w| Refusal::Config(format!("the rootfs tar: {w}")))?;
        let kind = entry.header().entry_type();
        let mode = entry.header().mode().unwrap_or(0o644);
        if mode & 0o6000 != 0 && kind != tar::EntryType::Directory {
            return Err(Refusal::Config(format!(
                "the rootfs tar: {} carries a setuid/setgid bit",
                rel.display()
            )));
        }
        // Nothing is written through a symlink: every parent of the entry must be a
        // real directory of this rootfs (a symlink parent, absolute or not, would be
        // followed to wherever it points on the host).
        through_no_symlink(&staging, &rel)?;
        match kind {
            tar::EntryType::Regular
            | tar::EntryType::Continuous
            | tar::EntryType::Directory
            | tar::EntryType::GNULongName
            | tar::EntryType::GNULongLink => {}
            tar::EntryType::Symlink => {
                let target = entry
                    .link_name()
                    .map_err(|e| io("a link", e))?
                    .map(|p| p.into_owned())
                    .unwrap_or_default();
                // Absolute: the container's own root, never followed here. Relative: it
                // must not climb out of the rootfs.
                if !target.is_absolute()
                    && target
                        .components()
                        .any(|c| matches!(c, std::path::Component::ParentDir))
                    && leaves_root(&rel, &target)
                {
                    return Err(Refusal::Config(format!(
                        "the rootfs tar: symlink {} -> {} leaves the root",
                        rel.display(),
                        target.display()
                    )));
                }
            }
            tar::EntryType::Link => {
                let target = entry
                    .link_name()
                    .map_err(|e| io("a link", e))?
                    .map(|p| p.into_owned())
                    .unwrap_or_default();
                checked_relative(&target).map_err(|w| {
                    Refusal::Config(format!("the rootfs tar: hard link {}: {w}", rel.display()))
                })?;
            }
            other => {
                return Err(Refusal::Config(format!(
                    "the rootfs tar: {} is a {other:?} entry (no device nodes, fifos or sockets)",
                    rel.display()
                )))
            }
        }
        let dest = staging.join(&rel);
        if rel.as_os_str().is_empty() {
            continue;
        }
        entry
            .unpack_in(&staging)
            .map_err(|e| io(&format!("unpack {}", rel.display()), e))?;
        if kind == tar::EntryType::Directory {
            use std::os::unix::fs::PermissionsExt as _;
            std::fs::set_permissions(
                &dest,
                std::fs::Permissions::from_mode((mode & 0o7777) | 0o700),
            )
            .map_err(|e| io(&format!("chmod {}", rel.display()), e))?;
            dirs.retain(|(d, _)| d != &dest);
            dirs.push((dest.clone(), mode & 0o7777));
        }
        if uid_shift != 0 {
            let uid = entry.header().uid().unwrap_or(0) as u32;
            let gid = entry.header().gid().unwrap_or(0) as u32;
            lchown(
                &dest,
                uid.saturating_add(uid_shift),
                gid.saturating_add(uid_shift),
            )?;
        }
        n += 1;
    }
    // Directories last, the deepest first: a read-only directory refuses the files under it
    // while unpacking.
    dirs.sort_by_key(|(d, _)| std::cmp::Reverse(d.components().count()));
    for (d, mode) in &dirs {
        use std::os::unix::fs::PermissionsExt as _;
        std::fs::set_permissions(d, std::fs::Permissions::from_mode(*mode))
            .map_err(|e| io(&format!("chmod {}", d.display()), e))?;
    }
    let _ = remove_tree(dir);
    std::fs::rename(&staging, dir).map_err(|e| {
        io(
            &format!("rename {} to {}", staging.display(), dir.display()),
            e,
        )
    })?;
    Ok(n)
}

/// **Remove a laid tree**, its read-only directories too: each directory is made writable
/// for its owner first (a non-root owner cannot unlink in a 0555 directory). Symlinks are
/// never followed.
pub fn remove_tree(dir: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt as _;
    fn open_up(d: &Path) {
        let Ok(m) = std::fs::symlink_metadata(d) else {
            return;
        };
        if !m.is_dir() {
            return;
        }
        let _ = std::fs::set_permissions(
            d,
            std::fs::Permissions::from_mode((m.permissions().mode() & 0o7777) | 0o700),
        );
        if let Ok(rd) = std::fs::read_dir(d) {
            for e in rd.flatten() {
                open_up(&e.path());
            }
        }
    }
    open_up(dir);
    std::fs::remove_dir_all(dir)
}

/// Every ancestor of `rel` under `root` that exists must be a directory and not
/// a symlink (so nothing is written through one).
fn through_no_symlink(root: &Path, rel: &Path) -> Result<(), Refusal> {
    let mut at = root.to_path_buf();
    let parts: Vec<_> = rel.components().collect();
    for c in parts.iter().take(parts.len().saturating_sub(1)) {
        at.push(c);
        match std::fs::symlink_metadata(&at) {
            Ok(m) if m.file_type().is_symlink() => {
                return Err(Refusal::Config(format!(
                    "the rootfs tar: {} would be written through the symlink {}",
                    rel.display(),
                    at.strip_prefix(root).unwrap_or(&at).display()
                )));
            }
            _ => {}
        }
    }
    Ok(())
}

fn leaves_root(at: &Path, target: &Path) -> bool {
    let mut depth = at.components().count().saturating_sub(1) as i64;
    for c in target.components() {
        match c {
            std::path::Component::ParentDir => {
                depth -= 1;
                if depth < 0 {
                    return true;
                }
            }
            std::path::Component::Normal(_) => depth += 1,
            _ => {}
        }
    }
    false
}

/// A relative path with no `..`, no root, no empty or `.` components beyond the first.
fn checked_relative(p: &Path) -> Result<PathBuf, String> {
    let mut out = PathBuf::new();
    for c in p.components() {
        match c {
            std::path::Component::Normal(n) => out.push(n),
            std::path::Component::CurDir => {}
            std::path::Component::RootDir | std::path::Component::Prefix(_) => {
                return Err(format!("{} is absolute", p.display()))
            }
            std::path::Component::ParentDir => {
                return Err(format!("{} climbs out (..)", p.display()))
            }
        }
    }
    Ok(out)
}

// ── re-hashing what was laid (NCSC P1, lane HOL-NCSC) ──────────────────────

/// **A laid tree, as words**: relative path → `d <mode>`, `f <mode> <sha256>` or `l <target>`,
/// each with ` <uid>:<gid>` when the owners were shifted. `d ?` is a parent directory the tar
/// does not name (laid with whatever mode the unpacking made).
pub type Tree = std::collections::BTreeMap<String, String>;

const IMPLIED_DIR: &str = "d ?";

fn owners(uid: u64, gid: u64, uid_shift: u32) -> String {
    if uid_shift == 0 {
        String::new()
    } else {
        format!(
            " {}:{}",
            uid.saturating_add(uid_shift as u64),
            gid.saturating_add(uid_shift as u64)
        )
    }
}

fn sha256_of(r: &mut dyn std::io::Read) -> std::io::Result<String> {
    use sha2::Digest as _;
    let mut h = sha2::Sha256::new();
    let mut buf = vec![0u8; 64 << 10];
    loop {
        let n = r.read(&mut buf)?;
        if n == 0 {
            break;
        }
        h.update(&buf[..n]);
    }
    Ok(h.finalize().iter().map(|x| format!("{x:02x}")).collect())
}

/// **What [`lay_rootfs`] lays from `tar_bytes`**, as a [`Tree`] (the same `uid_shift`).
pub fn tar_tree(tar_bytes: &[u8], uid_shift: u32) -> Result<Tree, Refusal> {
    let io = |what: &str, e: std::io::Error| Refusal::Io(format!("{what}: {e}"));
    let mut t = Tree::new();
    let mut a = tar::Archive::new(tar_bytes);
    for entry in a.entries().map_err(|e| io("the rootfs tar", e))? {
        let mut entry = entry.map_err(|e| io("the rootfs tar", e))?;
        let path = entry
            .path()
            .map_err(|e| io("an entry's path", e))?
            .into_owned();
        let rel =
            checked_relative(&path).map_err(|w| Refusal::Config(format!("the rootfs tar: {w}")))?;
        if rel.as_os_str().is_empty() {
            continue;
        }
        let key = rel.display().to_string();
        let h = entry.header();
        let mode = h.mode().unwrap_or(0o644) & 0o7777;
        let own = owners(h.uid().unwrap_or(0), h.gid().unwrap_or(0), uid_shift);
        let words = match h.entry_type() {
            tar::EntryType::Directory => format!("d {mode:o}{own}"),
            tar::EntryType::Regular | tar::EntryType::Continuous => {
                format!(
                    "f {mode:o} {}{own}",
                    sha256_of(&mut entry).map_err(|e| io(&key, e))?
                )
            }
            tar::EntryType::Symlink => {
                let target = entry
                    .link_name()
                    .map_err(|e| io("a link", e))?
                    .map(|p| p.display().to_string())
                    .unwrap_or_default();
                format!("l {target}{own}")
            }
            tar::EntryType::Link => {
                // One inode with its target: the target's mode and bytes; the owner the last
                // lay wrote (this entry's), on both names.
                let target = entry
                    .link_name()
                    .map_err(|e| io("a link", e))?
                    .map(|p| p.into_owned())
                    .unwrap_or_default();
                let tkey = checked_relative(&target)
                    .map_err(|w| Refusal::Config(format!("the rootfs tar: hard link {key}: {w}")))?
                    .display()
                    .to_string();
                let Some(tw) = t.get(&tkey).cloned() else {
                    return Err(Refusal::Config(format!("the rootfs tar: hard link {key} names {tkey}, which comes later or not at all")));
                };
                let parts: Vec<&str> = tw.split(' ').take(3).collect();
                let words = format!("{}{own}", parts.join(" "));
                t.insert(tkey, words.clone());
                words
            }
            _ => continue,
        };
        for anc in rel.ancestors().skip(1) {
            if anc.as_os_str().is_empty() {
                break;
            }
            t.entry(anc.display().to_string())
                .or_insert_with(|| IMPLIED_DIR.to_string());
        }
        t.insert(key, words);
    }
    Ok(t)
}

/// **What is laid at `dir`**, as a [`Tree`]. Symlinks are read, never followed.
pub fn dir_tree(dir: &Path, uid_shift: u32) -> Result<Tree, Refusal> {
    use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
    let io = |what: &Path, e: std::io::Error| Refusal::Io(format!("{}: {e}", what.display()));
    let mut t = Tree::new();
    let mut todo = vec![dir.to_path_buf()];
    while let Some(d) = todo.pop() {
        for e in std::fs::read_dir(&d).map_err(|e| io(&d, e))? {
            let p = e.map_err(|e| io(&d, e))?.path();
            let m = std::fs::symlink_metadata(&p).map_err(|e| io(&p, e))?;
            let key = p.strip_prefix(dir).unwrap_or(&p).display().to_string();
            let mode = m.permissions().mode() & 0o7777;
            // The laid owners as they are: the lay already shifted them (`uid + shift`); the
            // tar's side ([`tar_tree`]) adds the shift. Shifting here again made every laid
            // rootfs differ under a non-zero shift (lane HOL-CDS-5).
            let own = if uid_shift == 0 {
                String::new()
            } else {
                format!(" {}:{}", m.uid(), m.gid())
            };
            let ft = m.file_type();
            let words = if ft.is_symlink() {
                format!(
                    "l {}{own}",
                    std::fs::read_link(&p).map_err(|e| io(&p, e))?.display()
                )
            } else if ft.is_dir() {
                todo.push(p.clone());
                format!("d {mode:o}{own}")
            } else if ft.is_file() {
                let mut f = std::fs::File::open(&p).map_err(|e| io(&p, e))?;
                format!(
                    "f {mode:o} {}{own}",
                    sha256_of(&mut f).map_err(|e| io(&p, e))?
                )
            } else {
                format!("x {:?}", ft)
            };
            t.insert(key, words);
        }
    }
    Ok(t)
}

/// The sha256 of nothing: an empty file.
const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

/// **The first way `got` is not `want`**, in words, or `None`. A parent directory the tar does
/// not name may have any mode. A directory or an EMPTY file that is not in the tar is a mount
/// point the runtime made (crun makes its bind targets inside the root) and is not a change
/// (what a directory holds is read entry by entry); anything else beside the tar is.
pub fn tree_difference(want: &Tree, got: &Tree) -> Option<String> {
    for (path, w) in want {
        match got.get(path) {
            None => return Some(format!("{path} is missing")),
            Some(g) if w == IMPLIED_DIR => {
                if !g.starts_with("d ") {
                    return Some(format!(
                        "{path} is `{g}`, the medium lays a directory there"
                    ));
                }
            }
            Some(g) if g != w => return Some(format!("{path} is `{g}`, the medium says `{w}`")),
            Some(_) => {}
        }
    }
    for (path, g) in got.iter().filter(|(p, _)| !want.contains_key(*p)) {
        let mut f = g.split(' ');
        // A directory holds nothing by itself: what is in it is read entry by entry here.
        let dir = g.starts_with("d ")
            && f.nth(1)
                .and_then(|m| u32::from_str_radix(m, 8).ok())
                .is_some_and(|m| m & 0o4000 == 0);
        let mut f = g.split(' ');
        let empty_file = f.next() == Some("f")
            && f.next()
                .and_then(|m| u32::from_str_radix(m, 8).ok())
                .is_some_and(|m| m & 0o6000 == 0)
            && f.next() == Some(EMPTY_SHA256);
        if !(dir || empty_file) {
            return Some(format!("{path} (`{g}`) is not on the medium"));
        }
    }
    None
}

/// **Is the rootfs laid at `dir` still what `tar_bytes` lays?** `None` when it is, else the
/// first difference in words.
pub fn laid_rootfs_difference(
    tar_bytes: &[u8],
    dir: &Path,
    uid_shift: u32,
) -> Result<Option<String>, Refusal> {
    let want = tar_tree(tar_bytes, uid_shift)?;
    let got = dir_tree(dir, uid_shift)?;
    Ok(tree_difference(&want, &got))
}

/// **Is the program laid at `at` still the entry `sha256`** (executable, 0755)?
pub fn laid_program_difference(sha256: &str, at: &Path) -> Result<Option<String>, Refusal> {
    use std::os::unix::fs::PermissionsExt as _;
    let m =
        std::fs::symlink_metadata(at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    if !m.file_type().is_file() {
        return Ok(Some(format!("{} is not a plain file", at.display())));
    }
    let mode = m.permissions().mode() & 0o7777;
    if mode != 0o755 {
        return Ok(Some(format!(
            "{} has mode {mode:o}, laid 755",
            at.display()
        )));
    }
    let mut f =
        std::fs::File::open(at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    let got = sha256_of(&mut f).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    Ok((got != sha256).then(|| {
        format!(
            "{} has sha256 {got}, the medium says {sha256}",
            at.display()
        )
    }))
}

/// **Is the read-only file laid at `at` still the entry `sha256`** (0444)?
pub fn laid_file_difference(sha256: &str, at: &Path) -> Result<Option<String>, Refusal> {
    use std::os::unix::fs::PermissionsExt as _;
    let m =
        std::fs::symlink_metadata(at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    if !m.file_type().is_file() {
        return Ok(Some(format!("{} is not a plain file", at.display())));
    }
    let mode = m.permissions().mode() & 0o7777;
    if mode != 0o444 {
        return Ok(Some(format!(
            "{} has mode {mode:o}, laid 444",
            at.display()
        )));
    }
    let mut f =
        std::fs::File::open(at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    let got = sha256_of(&mut f).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    Ok((got != sha256).then(|| {
        format!(
            "{} has sha256 {got}, the medium says {sha256}",
            at.display()
        )
    }))
}

/// Lay a read-only file (a guest, the worker's rootfs tar): written whole, then 0444.
pub fn lay_file(bytes: &[u8], at: &Path) -> Result<(), Refusal> {
    use std::os::unix::fs::PermissionsExt as _;
    if let Some(d) = at.parent() {
        std::fs::create_dir_all(d).map_err(|e| Refusal::Io(format!("{}: {e}", d.display())))?;
    }
    let tmp = at.with_extension("laying");
    let _ = std::fs::remove_file(&tmp);
    std::fs::write(&tmp, bytes).map_err(|e| Refusal::Io(format!("{}: {e}", tmp.display())))?;
    std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o444))
        .map_err(|e| Refusal::Io(format!("{}: {e}", tmp.display())))?;
    std::fs::rename(&tmp, at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    Ok(())
}

/// `chown` without following a symlink (the link itself gets the owner).
pub fn lchown(p: &Path, uid: u32, gid: u32) -> Result<(), Refusal> {
    rustix::fs::chownat(
        rustix::fs::CWD,
        p,
        Some(rustix::fs::Uid::from_raw(uid)),
        Some(rustix::fs::Gid::from_raw(gid)),
        rustix::fs::AtFlags::SYMLINK_NOFOLLOW,
    )
    .map_err(|e| {
        Refusal::Io(format!(
            "chown {} to {uid}:{gid}: {e} (the sandbox is laid by root)",
            p.display()
        ))
    })
}

/// Lay the runtime program: written whole, then made executable.
pub fn lay_program(bytes: &[u8], at: &Path) -> Result<(), Refusal> {
    use std::os::unix::fs::PermissionsExt as _;
    if let Some(d) = at.parent() {
        std::fs::create_dir_all(d).map_err(|e| Refusal::Io(format!("{}: {e}", d.display())))?;
    }
    let tmp = at.with_extension("laying");
    std::fs::write(&tmp, bytes).map_err(|e| Refusal::Io(format!("{}: {e}", tmp.display())))?;
    std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))
        .map_err(|e| Refusal::Io(format!("{}: {e}", tmp.display())))?;
    std::fs::rename(&tmp, at).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))?;
    Ok(())
}

/// Read a file whole, naming it on failure.
pub fn read(p: &Path) -> Result<Vec<u8>, Refusal> {
    let mut f = std::fs::File::open(p).map_err(|e| Refusal::Io(format!("{}: {e}", p.display())))?;
    let mut v = Vec::new();
    f.read_to_end(&mut v)
        .map_err(|e| Refusal::Io(format!("{}: {e}", p.display())))?;
    Ok(v)
}

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

    #[test]
    fn the_worker_rootfs_tar_carries_the_image_as_files() {
        let t = worker_rootfs_tar(b"\x7fELF-worker");
        let mut a = tar::Archive::new(&t[..]);
        let mut seen = std::collections::BTreeMap::new();
        for e in a.entries().unwrap() {
            let e = e.unwrap();
            let p = e.path().unwrap().display().to_string();
            seen.insert(
                p,
                (
                    e.header().mode().unwrap(),
                    e.header().uid().unwrap(),
                    e.header().entry_type(),
                ),
            );
        }
        assert_eq!(
            seen["holger-scan-worker"],
            (0o555, 0, tar::EntryType::Regular)
        );
        assert_eq!(seen["etc/passwd"].0, 0o444);
        assert_eq!(seen["scratch"], (0o1777, 0, tar::EntryType::Directory));
        for p in [
            "in",
            "in/d-yara",
            "proc",
            "dev",
            "sys",
            "tmp",
            "in/artifact",
            "in/job.json",
        ] {
            assert!(seen.contains_key(p), "{p} in {:?}", seen.keys());
        }
        // A job's mount points (lane HOL-SANDBOX-TRAIT): crun binds onto a READ-ONLY root, so
        // every target is laid here: the job's input /in/i, its read paths /in/r0.. and /out.
        for p in ["in/i", "in/r0", "in/r1", "in/r2", "in/r3", "out"] {
            assert_eq!(
                seen.get(p).map(|e| e.2),
                Some(tar::EntryType::Directory),
                "the mount point {p} in {:?}",
                seen.keys()
            );
        }
        assert_eq!(
            worker_rootfs_tar(b"x"),
            worker_rootfs_tar(b"x"),
            "deterministic: the same bytes every time"
        );
    }

    #[test]
    fn the_manifest_round_trips_and_verifies_each_entry() {
        let m = Manifest::of(b"crun", b"worker", b"clamav");
        m.check().unwrap();
        let text = toml::to_string(&m).unwrap();
        assert!(
            text.contains("[[entry]]")
                && text.contains("name = \"crun\"")
                && text.contains("kind = \"program\""),
            "{text}"
        );
        let back: Manifest = toml::from_str(&text).unwrap();
        assert_eq!(back, m);
        back.verify(CRUN, b"crun").unwrap();
        let e = back.verify(CRUN, b"crux").unwrap_err();
        assert_eq!(e.code().code, "holger-sandbox-8", "{e}");
        assert!(e.to_string().contains("sha256"), "{e}");
        let e = back.verify(CRUN, b"crun-longer").unwrap_err();
        assert!(e.to_string().contains("bytes"), "{e}");
        assert!(back.verify("other", b"").is_err());
        let bad = Manifest {
            schema: "holger.sandbox-media/v9".into(),
            ..m.clone()
        };
        assert!(bad.check().unwrap_err().to_string().contains("schema"));
        let short = Manifest {
            entries: m.entries[..2].to_vec(),
            ..m.clone()
        };
        assert!(short
            .check()
            .unwrap_err()
            .to_string()
            .contains(CLAMAV_ROOTFS));
        let traversal = Manifest {
            entries: vec![Entry {
                name: "../x".into(),
                ..m.entries[0].clone()
            }],
            ..m.clone()
        };
        assert!(traversal.check().is_err());
    }

    #[test]
    fn the_manifest_refuses_unknown_keys() {
        assert!(
            toml::from_str::<Manifest>("schema = \"holger.sandbox-media/v1\"\nextra = 1\n")
                .is_err()
        );
    }

    #[test]
    fn a_rootfs_tar_is_laid_hardened() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("rootfs");
        let n = lay_rootfs(&worker_rootfs_tar(b"\x7fELF"), &root, 0).unwrap();
        assert!(n >= 10);
        assert_eq!(
            std::fs::read(root.join("holger-scan-worker")).unwrap(),
            b"\x7fELF"
        );
        use std::os::unix::fs::PermissionsExt as _;
        assert_eq!(
            std::fs::metadata(root.join("holger-scan-worker"))
                .unwrap()
                .permissions()
                .mode()
                & 0o7777,
            0o555
        );
        assert_eq!(
            std::fs::metadata(root.join("scratch"))
                .unwrap()
                .permissions()
                .mode()
                & 0o7777,
            0o1777
        );
        // Laid again: replaced whole, nothing stale kept.
        std::fs::write(root.join("stale"), b"x").unwrap();
        lay_rootfs(&worker_rootfs_tar(b"\x7fELF2"), &root, 0).unwrap();
        assert!(!root.join("stale").exists());
        assert_eq!(
            std::fs::read(root.join("holger-scan-worker")).unwrap(),
            b"\x7fELF2"
        );
    }

    /// The directories' own modes are laid last, the deepest first: a read-only directory
    /// before its children (`in/` 0555, then `in/d-yara` 0555 and `in/job.json`) lays for a
    /// user without CAP_DAC_OVERRIDE, keeps its mode, and is removed by [`remove_tree`].
    #[test]
    fn a_read_only_directory_is_laid_with_its_children_and_removed() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("rootfs");
        lay_rootfs(&worker_rootfs_tar(b"\x7fELF"), &root, 0).unwrap();
        for d in ["in", "in/d-yara", "proc", "sys"] {
            assert_eq!(
                std::fs::metadata(root.join(d))
                    .unwrap()
                    .permissions()
                    .mode()
                    & 0o7777,
                0o555,
                "{d}"
            );
        }
        assert!(root.join("in/job.json").is_file() && root.join("in/d-yara").is_dir());
        remove_tree(&root).expect("a laid tree is removed whole");
        assert!(!root.exists());
    }

    /// A tar entry with its name and link fields written by hand: the `tar`
    /// crate's builder refuses `..` and absolute names, and a hostile archive
    /// does not use the builder.
    fn tar_with(path: &str, kind: tar::EntryType, mode: u32, link: Option<&str>) -> Vec<u8> {
        let mut h = tar::Header::new_gnu();
        h.set_entry_type(kind);
        h.set_mode(mode);
        h.set_size(0);
        h.set_uid(0);
        h.set_gid(0);
        {
            let g = h.as_gnu_mut().unwrap();
            g.name[..path.len()].copy_from_slice(path.as_bytes());
            if let Some(l) = link {
                g.linkname[..l.len()].copy_from_slice(l.as_bytes());
            }
        }
        h.set_cksum();
        let mut t = tar::Builder::new(Vec::new());
        t.append(&h, std::io::empty()).unwrap();
        t.into_inner().unwrap()
    }

    /// Two entries in one tar, by hand (the second may write through the first).
    fn tar_with_two(
        a: (&str, tar::EntryType, u32, Option<&str>),
        b: (&str, tar::EntryType, u32, Option<&str>),
    ) -> Vec<u8> {
        let mut out = Vec::new();
        for (path, kind, mode, link) in [a, b] {
            let one = tar_with(path, kind, mode, link);
            out.extend_from_slice(&one[..one.len() - 1024]); // drop the end-of-archive blocks
        }
        out.extend_from_slice(&[0u8; 1024]);
        out
    }

    #[test]
    fn a_hostile_rootfs_tar_is_refused_by_name() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("r");
        let outside = dir.path().join("outside");
        std::fs::create_dir_all(&outside).unwrap();
        let outside_s = outside.display().to_string();
        let cases: Vec<(Vec<u8>, &str)> = vec![
            (
                tar_with("../escape", tar::EntryType::Regular, 0o644, None),
                "climbs out",
            ),
            (
                tar_with("/abs", tar::EntryType::Regular, 0o644, None),
                "absolute",
            ),
            (
                tar_with("bin/su", tar::EntryType::Regular, 0o4755, None),
                "setuid",
            ),
            (
                tar_with("dev/sda", tar::EntryType::Block, 0o600, None),
                "device",
            ),
            (
                tar_with("a/b", tar::EntryType::Symlink, 0o777, Some("../../../x")),
                "leaves the root",
            ),
            (
                tar_with("a/l", tar::EntryType::Link, 0o644, Some("../../x")),
                "climbs out",
            ),
            // Written THROUGH a symlink that points outside: refused before the write.
            (
                tar_with_two(
                    ("lib", tar::EntryType::Symlink, 0o777, Some(&outside_s)),
                    ("lib/evil", tar::EntryType::Regular, 0o644, None),
                ),
                "through the symlink",
            ),
            (
                tar_with_two(
                    ("etc", tar::EntryType::Symlink, 0o777, Some("/etc")),
                    ("etc/passwd", tar::EntryType::Regular, 0o644, None),
                ),
                "through the symlink",
            ),
        ];
        for (t, why) in cases {
            let e = lay_rootfs(&t, &root, 0).unwrap_err();
            assert!(e.to_string().contains(why), "{why}: {e}");
            assert!(!root.exists(), "nothing laid after a refusal: {why}");
        }
        assert!(
            !outside.join("evil").exists(),
            "nothing reached the outside directory"
        );
        // What a real image carries is fine: a relative symlink that stays inside, an
        // ABSOLUTE symlink (busybox), a setgid directory (ClamAV's database directory).
        lay_rootfs(
            &tar_with(
                "usr/bin/clamd",
                tar::EntryType::Symlink,
                0o777,
                Some("../sbin/clamd"),
            ),
            &root,
            0,
        )
        .unwrap();
        lay_rootfs(
            &tar_with(
                "bin/arch",
                tar::EntryType::Symlink,
                0o777,
                Some("/bin/busybox"),
            ),
            &root,
            0,
        )
        .unwrap();
        let m = std::fs::symlink_metadata(root.join("bin/arch")).unwrap();
        assert!(
            m.file_type().is_symlink(),
            "laid as a symlink, not followed"
        );
        assert_eq!(
            std::fs::read_link(root.join("bin/arch")).unwrap(),
            PathBuf::from("/bin/busybox")
        );
        lay_rootfs(
            &tar_with("var/lib/clamav", tar::EntryType::Directory, 0o2755, None),
            &root,
            0,
        )
        .unwrap();
        use std::os::unix::fs::PermissionsExt as _;
        assert_eq!(
            std::fs::metadata(root.join("var/lib/clamav"))
                .unwrap()
                .permissions()
                .mode()
                & 0o7777,
            0o2755
        );
    }

    /// ★ **A rootfs laid under a user-namespace shift reads back as its tar** (lane HOL-CDS-5):
    /// the lay owns each entry `uid + shift`, and the read-back takes the laid owners as they
    /// are; it must not shift them again (on the appliance, PID 1 as root with the base 100000,
    /// every start refused `holger-sandbox-14`: `dev` is `200000:200000`, the medium says
    /// `100000:100000`). The shift is this user's own uid when it is not root (a chown to
    /// yourself needs no privilege), so the test runs anywhere.
    #[test]
    fn a_rootfs_laid_under_a_shift_reads_back_as_its_tar() {
        use std::os::unix::fs::MetadataExt as _;
        let dir = tempfile::tempdir().unwrap();
        let me = std::fs::metadata(dir.path()).unwrap().uid();
        let shift = if me == 0 { 100_000 } else { me };
        let root = dir.path().join("rootfs");
        let tar = worker_rootfs_tar(b"\x7fELF");
        lay_rootfs(&tar, &root, shift).unwrap();
        assert_eq!(
            laid_rootfs_difference(&tar, &root, shift).unwrap(),
            None,
            "laid at +{shift}, read back at +{shift}"
        );
    }

    #[test]
    fn a_laid_rootfs_reads_back_as_its_tar() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("rootfs");
        let tar = worker_rootfs_tar(b"\x7fELF");
        lay_rootfs(&tar, &root, 0).unwrap();
        assert_eq!(laid_rootfs_difference(&tar, &root, 0).unwrap(), None);
        // A parent the tar does not name, and an absolute symlink: laid and read back equal.
        let t = tar_with(
            "usr/bin/arch",
            tar::EntryType::Symlink,
            0o777,
            Some("/bin/busybox"),
        );
        lay_rootfs(&t, &root, 0).unwrap();
        assert_eq!(laid_rootfs_difference(&t, &root, 0).unwrap(), None);
        std::fs::remove_file(root.join("usr/bin/arch")).unwrap();
        std::os::unix::fs::symlink("/bin/evil", root.join("usr/bin/arch")).unwrap();
        let why = laid_rootfs_difference(&t, &root, 0).unwrap().unwrap();
        assert!(
            why.contains("usr/bin/arch") && why.contains("/bin/evil"),
            "{why}"
        );
        // An extra file with bytes is a change; an empty one (a runtime's mount point) is not.
        lay_rootfs(&tar, &root, 0).unwrap();
        std::fs::write(root.join("tmp/x"), b"").unwrap();
        assert_eq!(laid_rootfs_difference(&tar, &root, 0).unwrap(), None);
        std::fs::write(root.join("tmp/x"), b"#!").unwrap();
        assert!(laid_rootfs_difference(&tar, &root, 0)
            .unwrap()
            .unwrap()
            .contains("tmp/x"));
    }

    /// A real image's rootfs (`HOLGER_TEST_ROOTFS_TAR`, e.g. the pinned ClamAV image flattened by
    /// `podman export`: 254 hard links, 368 symlinks): laid, it reads back as its tar. Skipped
    /// without the variable.
    #[test]
    fn a_real_rootfs_reads_back_as_its_tar() {
        let Some(p) = std::env::var_os("HOLGER_TEST_ROOTFS_TAR") else {
            return;
        };
        let bytes = std::fs::read(&p).unwrap();
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("rootfs");
        lay_rootfs(&bytes, &root, 0).unwrap();
        assert_eq!(laid_rootfs_difference(&bytes, &root, 0).unwrap(), None);
    }

    #[test]
    fn a_laid_program_is_checked_by_its_bytes_and_its_mode() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("crun");
        lay_program(b"crun", &at).unwrap();
        assert_eq!(
            laid_program_difference(&sha256_hex(b"crun"), &at).unwrap(),
            None
        );
        std::fs::set_permissions(&at, std::fs::Permissions::from_mode(0o4755)).unwrap();
        assert!(laid_program_difference(&sha256_hex(b"crun"), &at)
            .unwrap()
            .unwrap()
            .contains("mode"));
        lay_program(b"crux", &at).unwrap();
        assert!(laid_program_difference(&sha256_hex(b"crun"), &at)
            .unwrap()
            .unwrap()
            .contains("sha256"));
    }

    #[test]
    fn the_layout_is_under_the_data_root() {
        let l = Layout::under(Path::new("/data/holger"));
        assert_eq!(l.crun(), PathBuf::from("/data/holger/sandbox/crun"));
        assert_eq!(
            l.worker_rootfs(),
            PathBuf::from("/data/holger/sandbox/worker/rootfs")
        );
        assert_eq!(
            l.clamd_socket(),
            PathBuf::from("/data/holger/sandbox/clamd/clamd.sock")
        );
        assert_eq!(entry_path(MANIFEST), "sandbox/manifest.toml");
    }

    #[test]
    fn the_program_is_laid_executable() {
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("bin").join("crun");
        lay_program(b"#!/bin/sh\n", &at).unwrap();
        use std::os::unix::fs::PermissionsExt as _;
        assert_eq!(
            std::fs::metadata(&at).unwrap().permissions().mode() & 0o777,
            0o755
        );
    }
}