server/lib/src/sandbox/media.rs
//! **The sandbox laid from the medium** (lane HOLGER-MEDIA, 2026-10-23): the
//! box's half of `holger_sandbox::media`.
//!
//! Before the server serves, every entry the medium carries beside the app
//! (the manifest, crun, the worker's rootfs, ClamAV's rootfs) is fetched out of
//! the running slot's signed archive, checked against the manifest's sha256 and
//! laid under `<data_root>/sandbox` ([`Layout`]). A record of what was laid
//! (`laid.toml`: entry → sha256) makes the next boot of the same medium lay
//! nothing again, but EVERY boot re-hashes (NCSC P1, lane HOL-NCSC): the
//! medium's entries against the manifest, and what is laid against them (every
//! file's sha256, mode, owner and link); a laid tree that differs is laid again
//! from the medium and said by name (`holger-sandbox-14`), and one that still
//! differs refuses the scans. A new release (a keep medium, a live update)
//! carries new entries and lays them over. One entry that is missing, short or wrong refuses
//! the lay by name and NOTHING of it is kept: the scans stay refused
//! (`holger-sandbox-1`).
//!
//! `fetch` is the one seam: on the appliance it reads an entry out of the
//! running slot's archive (server-cli `sandbox_boot`); a test, a container or a
//! developer's box names a directory of entries (`HOLGER_SANDBOX_MEDIA_DIR`,
//! the shape `cargo xtask scan-worker stage` writes).
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use holger_sandbox::media::{self as m, Kind, Layout, Manifest};
use holger_sandbox::Refusal;
use serde::{Deserialize, Serialize};
/// A directory of entries instead of the running slot's archive.
pub const MEDIA_DIR_ENV: &str = "HOLGER_SANDBOX_MEDIA_DIR";
/// clamd's config inside its container (`clamd.conf` beside this file, the one
/// `scripts/clamd-sandbox.sh` mounts), laid beside its socket. It lives inside the crate so
/// the published crate carries it (it was `distribution/scan-worker/clamd.conf` until
/// PUB-HOLGER, 2026-10-28).
pub const CLAMD_CONF: &str = include_str!("clamd.conf");
/// What one lay did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Laid {
pub entries: Vec<LaidEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaidEntry {
pub name: String,
pub sha256: String,
/// Already laid by an earlier boot of the same medium, and re-hashed equal to it this
/// start: nothing written.
pub kept: bool,
/// Laid by an earlier boot, but what is on the disk no longer is what the medium says
/// (`holger-sandbox-14`): laid again from the medium this start.
pub drifted: bool,
}
impl Laid {
/// One line for the boot log.
pub fn words(&self) -> String {
self.entries
.iter()
.map(|e| {
let how = match (e.kept, e.drifted) {
(true, _) => " (kept, re-hashed)",
(false, true) => {
" (laid again: it differed from the medium, holger-sandbox-14)"
}
(false, false) => "",
};
format!("{} {}{how}", e.name, &e.sha256[..12])
})
.collect::<Vec<_>>()
.join(", ")
}
}
/// The record on disk: entry → sha256 of what was laid.
#[derive(Debug, Default, Serialize, Deserialize)]
struct Record {
#[serde(default)]
laid: BTreeMap<String, String>,
}
fn read_record(at: &Path) -> Record {
std::fs::read_to_string(at)
.ok()
.and_then(|t| toml::from_str(&t).ok())
.unwrap_or_default()
}
fn write_record(at: &Path, r: &Record) -> Result<(), Refusal> {
let text = toml::to_string(r).map_err(|e| Refusal::Io(format!("the laid record: {e}")))?;
std::fs::write(at, text).map_err(|e| Refusal::Io(format!("{}: {e}", at.display())))
}
/// Where an entry lands.
fn target_of(layout: &Layout, e: &m::Entry) -> PathBuf {
match e.name.as_str() {
m::CRUN => layout.crun(),
m::WORKER_ROOTFS => layout.worker_rootfs(),
m::CLAMAV_ROOTFS => layout.clamav_rootfs(),
other => match e.kind {
Kind::Program => layout.dir.join(other),
Kind::Guest => layout.heimdall_guests().join(other),
Kind::Rootfs => layout
.dir
.join(other.trim_end_matches(".tar"))
.join("rootfs"),
},
}
}
/// The first way the laid `target` is not the entry `e` (its `bytes`), or `None`.
fn difference(
e: &m::Entry,
bytes: &[u8],
target: &Path,
userns_base: u32,
) -> Result<Option<String>, Refusal> {
match e.kind {
Kind::Program => m::laid_program_difference(&e.sha256, target),
Kind::Rootfs => m::laid_rootfs_difference(bytes, target, userns_base),
Kind::Guest => m::laid_file_difference(&e.sha256, target),
}
}
/// **Lay the sandbox** under `layout` from `fetch`, with the containers' user
/// namespace base `userns_base` (0 on a developer's box: owners stay).
pub fn lay(
layout: &Layout,
fetch: &dyn Fn(&str) -> Result<Vec<u8>, Refusal>,
userns_base: u32,
) -> Result<Laid, Refusal> {
std::fs::create_dir_all(&layout.dir)
.map_err(|e| Refusal::Io(format!("{}: {e}", layout.dir.display())))?;
let text = fetch(m::MANIFEST)?;
let manifest: Manifest = toml::from_str(&String::from_utf8_lossy(&text))
.map_err(|e| Refusal::Config(format!("the sandbox manifest does not read: {e}")))?;
manifest.check()?;
let record_at = layout.laid_record();
let mut record = read_record(&record_at);
let mut laid = Vec::new();
for e in &manifest.entries {
let target = target_of(layout, e);
// EVERY start (NCSC P1, lane HOL-NCSC): the medium's entry against the manifest, then
// what is laid against the entry. The record only says what an earlier start laid; the
// data disk is not trusted to have kept it.
let bytes = fetch(&e.name)?;
manifest.verify(&e.name, &bytes)?;
let mut drifted = false;
if record.laid.get(&e.name) == Some(&e.sha256) && target.exists() {
match difference(e, &bytes, &target, userns_base)? {
None => {
laid.push(LaidEntry {
name: e.name.clone(),
sha256: e.sha256.clone(),
kept: true,
drifted: false,
});
continue;
}
Some(why) => {
log::warn!(
"{}",
Refusal::RootfsDrift(format!(
"sandbox entry {}: {why}; laid again from the medium",
e.name
))
);
drifted = true;
}
}
}
// Forget the stale record first: a lay that dies halfway lays again next boot.
record.laid.remove(&e.name);
write_record(&record_at, &record)?;
match e.kind {
Kind::Program => m::lay_program(&bytes, &target)?,
Kind::Rootfs => {
m::lay_rootfs(&bytes, &target, userns_base)?;
}
Kind::Guest => m::lay_file(&bytes, &target)?,
}
// Laid, and read back: what the scans run on is what the medium says, or nothing.
if let Some(why) = difference(e, &bytes, &target, userns_base)? {
return Err(Refusal::RootfsDrift(format!(
"sandbox entry {} laid from the medium still differs from it: {why}",
e.name
)));
}
record.laid.insert(e.name.clone(), e.sha256.clone());
write_record(&record_at, &record)?;
laid.push(LaidEntry {
name: e.name.clone(),
sha256: e.sha256.clone(),
kept: false,
drifted,
});
}
// Lane HOL-HEIMDALL-03: a medium that carries Heimdall gets the worker's rootfs tar laid
// whole too (its bytes verified above against the manifest this start), the one
// `engine: heimdall` mints its pinned worker image from; written again whenever it differs.
if manifest.carries_heimdall() {
let w = manifest
.entry(m::WORKER_ROOTFS)
.ok_or_else(|| Refusal::Config("the sandbox manifest names no worker rootfs".into()))?;
let at = layout.worker_rootfs_tar();
if !matches!(m::laid_file_difference(&w.sha256, &at), Ok(None)) {
let bytes = fetch(m::WORKER_ROOTFS)?;
manifest.verify(m::WORKER_ROOTFS, &bytes)?;
m::lay_file(&bytes, &at)?;
}
for d in [layout.heimdall_oci(), layout.heimdall_work()] {
std::fs::create_dir_all(&d)
.map_err(|e| Refusal::Io(format!("{}: {e}", d.display())))?;
}
}
for d in [layout.jobs(), layout.state(), layout.clamd_run()] {
std::fs::create_dir_all(&d).map_err(|e| Refusal::Io(format!("{}: {e}", d.display())))?;
}
// clamd's run directory is the container's (its socket appears there): owned by
// clamav's uid inside, which is base + 100 outside; world-writable for a base of 0.
let run = layout.clamd_run();
if userns_base != 0 {
m::lchown(
&run,
userns_base + super::clamd::CLAMD_UID,
userns_base + super::clamd::CLAMD_GID,
)?;
} else {
use std::os::unix::fs::PermissionsExt as _;
let _ = std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o1777));
}
std::fs::write(run.join("clamd.conf"), CLAMD_CONF)
.map_err(|e| Refusal::Io(format!("clamd.conf: {e}")))?;
Ok(Laid { entries: laid })
}
/// Entries out of the app archive (`sandbox/<name>` in the `.znippy` korp-start
/// verified at boot): the running slot's archive, staged by server-cli.
#[cfg(feature = "sign")]
pub fn fetch_from_archive(archive: PathBuf) -> impl Fn(&str) -> Result<Vec<u8>, Refusal> {
move |name: &str| {
znippy_common::get_file(&archive, &m::entry_path(name))
.map_err(|e| Refusal::ImageMissing(format!("sandbox entry {:?} is not in the app archive {} ({e}): this medium carries no scan sandbox", m::entry_path(name), archive.display())))
}
}
/// **Is `path` on a mount that forbids exec?** The data volume korp lays is
/// `nosuid,nodev,noexec` (korp-installer `DATA_XFS_MOUNT_FLAGS_NAMED`), and the
/// sandbox's runtime and rootfs live under it: MEASURED on the laid box
/// (2026-10-24), `crun --version: Permission denied` with a 0755 file as root.
/// The deepest mount point that is a prefix of `path` decides; `None` when
/// `mountinfo` names no mount above it (a test, an odd namespace).
pub fn mount_noexec_for(mountinfo: &str, path: &Path) -> Option<bool> {
let mut best: Option<(usize, bool)> = None;
for line in mountinfo.lines() {
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
let mut f = line.split_whitespace();
let (Some(_), Some(_), Some(_), Some(_), Some(mp), Some(opts)) =
(f.next(), f.next(), f.next(), f.next(), f.next(), f.next())
else {
continue;
};
let mp = mp.replace("\\040", " ");
let mp_path = Path::new(&mp);
if path.starts_with(mp_path) && best.is_none_or(|(l, _)| mp.len() > l) {
best = Some((mp.len(), opts.split(',').any(|o| o == "noexec")));
}
}
best.map(|(_, noexec)| noexec)
}
/// Entries from a directory (`<dir>/<name>`): the shape `cargo xtask scan-worker stage` writes.
pub fn fetch_from_dir(dir: PathBuf) -> impl Fn(&str) -> Result<Vec<u8>, Refusal> {
move |name: &str| {
let p = dir.join(name);
m::read(&p).map_err(|_| {
Refusal::ImageMissing(format!(
"sandbox entry {name:?} is not in {} (cargo xtask scan-worker stage)",
dir.display()
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn staged(dir: &Path) -> Manifest {
let worker = m::worker_rootfs_tar(b"\x7fELF-worker");
let clam = m::worker_rootfs_tar(b"\x7fELF-clamd");
let manifest = Manifest::of(b"#!/bin/sh\necho crun\n", &worker, &clam);
std::fs::write(dir.join(m::MANIFEST), toml::to_string(&manifest).unwrap()).unwrap();
std::fs::write(dir.join(m::CRUN), b"#!/bin/sh\necho crun\n").unwrap();
std::fs::write(dir.join(m::WORKER_ROOTFS), &worker).unwrap();
std::fs::write(dir.join(m::CLAMAV_ROOTFS), &clam).unwrap();
manifest
}
#[test]
fn the_sandbox_is_laid_from_the_entries_and_kept_on_the_next_boot() {
let tmp = tempfile::tempdir().unwrap();
let media = tmp.path().join("media");
std::fs::create_dir_all(&media).unwrap();
staged(&media);
let layout = Layout::under(&tmp.path().join("data"));
let fetch = fetch_from_dir(media.clone());
let first = lay(&layout, &fetch, 0).unwrap();
assert_eq!(first.entries.len(), 3);
assert!(first.entries.iter().all(|e| !e.kept));
assert!(layout.crun().is_file());
assert!(layout.worker_rootfs().join("holger-scan-worker").is_file());
assert!(layout.clamav_rootfs().join("holger-scan-worker").is_file());
assert!(layout.clamd_run().join("clamd.conf").is_file());
assert!(layout.jobs().is_dir() && layout.state().is_dir());
let again = lay(&layout, &fetch, 0).unwrap();
assert!(again.entries.iter().all(|e| e.kept), "{again:?}");
assert!(
again.words().contains("(kept, re-hashed)"),
"{}",
again.words()
);
// A new release: one entry differs, only that one is laid again.
let worker2 = m::worker_rootfs_tar(b"\x7fELF-worker-2");
let mut manifest = staged(&media);
manifest.entries[1].sha256 = m::sha256_hex(&worker2);
manifest.entries[1].bytes = worker2.len() as u64;
std::fs::write(media.join(m::MANIFEST), toml::to_string(&manifest).unwrap()).unwrap();
std::fs::write(media.join(m::WORKER_ROOTFS), &worker2).unwrap();
let third = lay(&layout, &fetch, 0).unwrap();
let kept: Vec<bool> = third.entries.iter().map(|e| e.kept).collect();
assert_eq!(kept, [true, false, true]);
assert_eq!(
std::fs::read(layout.worker_rootfs().join("holger-scan-worker")).unwrap(),
b"\x7fELF-worker-2"
);
}
/// NCSC P1 (lane HOL-NCSC): every start re-hashes what was laid. A changed file, an added
/// program or a changed runtime is found and laid again from the medium, by name.
#[test]
fn every_start_re_hashes_the_laid_sandbox_and_lays_a_changed_one_again() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = tempfile::tempdir().unwrap();
let media = tmp.path().join("media");
std::fs::create_dir_all(&media).unwrap();
staged(&media);
let layout = Layout::under(&tmp.path().join("data"));
let fetch = fetch_from_dir(media.clone());
lay(&layout, &fetch, 0).unwrap();
// Someone with the data disk changes the worker, adds a program and swaps the runtime.
let worker = layout.worker_rootfs().join("holger-scan-worker");
std::fs::set_permissions(&worker, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::write(&worker, b"\x7fELF-evil!").unwrap();
std::fs::set_permissions(&worker, std::fs::Permissions::from_mode(0o555)).unwrap();
let etc = layout.clamav_rootfs().join("etc");
std::fs::set_permissions(&etc, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::write(etc.join("evil.sh"), b"#!/bin/sh\nexit 0\n").unwrap();
std::fs::write(layout.crun(), b"#!/bin/sh\necho evil\n").unwrap();
let again = lay(&layout, &fetch, 0).unwrap();
let drifted: Vec<(&str, bool, bool)> = again
.entries
.iter()
.map(|e| (e.name.as_str(), e.kept, e.drifted))
.collect();
assert_eq!(
drifted,
[
(m::CRUN, false, true),
(m::WORKER_ROOTFS, false, true),
(m::CLAMAV_ROOTFS, false, true)
]
);
assert!(
again.words().contains("holger-sandbox-14"),
"{}",
again.words()
);
assert_eq!(
std::fs::read(&worker).unwrap(),
b"\x7fELF-worker",
"the medium's worker is back"
);
assert!(
!layout.clamav_rootfs().join("etc/evil.sh").exists(),
"the added program is gone"
);
assert_eq!(
std::fs::read(layout.crun()).unwrap(),
b"#!/bin/sh\necho crun\n"
);
// An empty mount point the runtime made (crun makes its bind targets) is not a change.
std::fs::set_permissions(&etc, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::write(layout.clamav_rootfs().join("etc/clamd-sandbox.conf"), b"").unwrap();
std::fs::create_dir_all(layout.clamav_rootfs().join("run/clamd")).unwrap();
let third = lay(&layout, &fetch, 0).unwrap();
assert!(
third.entries.iter().all(|e| e.kept && !e.drifted),
"{third:?}"
);
}
/// The medium itself is checked at every start, not only at the first lay: an entry that
/// changed under the manifest since is refused, though the record says it was laid.
#[test]
fn a_medium_entry_that_changed_since_the_first_lay_is_refused() {
let tmp = tempfile::tempdir().unwrap();
let media = tmp.path().join("media");
std::fs::create_dir_all(&media).unwrap();
staged(&media);
let layout = Layout::under(&tmp.path().join("data"));
lay(&layout, &fetch_from_dir(media.clone()), 0).unwrap();
let mut w = std::fs::read(media.join(m::WORKER_ROOTFS)).unwrap();
let at = w.len() / 3;
w[at] ^= 0x55;
std::fs::write(media.join(m::WORKER_ROOTFS), &w).unwrap();
let e = lay(&layout, &fetch_from_dir(media), 0).unwrap_err();
assert_eq!(e.code().code, "holger-sandbox-8", "{e}");
}
#[test]
fn a_wrong_or_missing_entry_refuses_the_lay_by_name_and_keeps_nothing_of_it() {
let tmp = tempfile::tempdir().unwrap();
let media = tmp.path().join("media");
std::fs::create_dir_all(&media).unwrap();
staged(&media);
// The same length, other bytes: the sha256 leg is the one that says no.
std::fs::write(media.join(m::CRUN), b"#!/bin/sh\necho crux\n").unwrap();
let layout = Layout::under(&tmp.path().join("data"));
let e = lay(&layout, &fetch_from_dir(media.clone()), 0).unwrap_err();
assert_eq!(e.code().code, "holger-sandbox-8", "{e}");
assert!(
e.to_string().contains("crun") && e.to_string().contains("sha256"),
"{e}"
);
assert!(!layout.crun().exists(), "nothing laid for the wrong entry");
assert!(
!layout.worker_rootfs().exists(),
"the lay stops at the first refusal"
);
std::fs::remove_file(media.join(m::CRUN)).unwrap();
let e = lay(&layout, &fetch_from_dir(media), 0).unwrap_err();
assert!(e.to_string().contains("scan-worker stage"), "{e}");
}
#[test]
fn the_governing_mount_decides_exec() {
let mi = "22 1 8:1 / / rw,relatime - xfs /dev/vda2 rw\n\
40 22 8:3 / /data rw,nosuid,nodev,noexec,relatime - xfs /dev/vdb1 rw\n\
41 40 0:30 / /data/holger/sandbox rw,nosuid,nodev,relatime - xfs /dev/vdb1 rw\n\
42 22 0:31 / /run/holger\\040x rw,noexec - tmpfs tmpfs rw\n";
assert_eq!(
mount_noexec_for(mi, Path::new("/data/holger/intel")),
Some(true)
);
assert_eq!(
mount_noexec_for(mi, Path::new("/data/holger/sandbox/crun")),
Some(false),
"the exec bind over the sandbox wins"
);
assert_eq!(mount_noexec_for(mi, Path::new("/etc/holger")), Some(false));
assert_eq!(
mount_noexec_for(mi, Path::new("/run/holger x/y")),
Some(true),
"an escaped mount point"
);
assert_eq!(mount_noexec_for("", Path::new("/x")), None);
}
#[test]
fn no_manifest_is_a_refusal_by_name() {
let tmp = tempfile::tempdir().unwrap();
let layout = Layout::under(&tmp.path().join("data"));
let e = lay(&layout, &fetch_from_dir(tmp.path().join("empty")), 0).unwrap_err();
assert!(e.to_string().contains("manifest.toml"), "{e}");
}
/// Lane HOL-HEIMDALL-03: a medium that carries Heimdall's shim and its released guests lays
/// the shim executable, every guest file read-only under `heimdall-guests/`, and the worker's
/// rootfs tar whole (what `engine: heimdall` mints its image from); each re-hashed at every
/// start, a changed one laid again. A medium without the shim lays none of it.
#[test]
fn a_medium_with_heimdall_lays_its_shim_its_guests_and_the_worker_tar() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = tempfile::tempdir().unwrap();
let media = tmp.path().join("media");
std::fs::create_dir_all(&media).unwrap();
let shim = b"\x7fELF-heimdall-container-shim".to_vec();
let guest = b"\0asm-verifier".to_vec();
let sig = b"cms".to_vec();
let manifest = staged(&media)
.with(
m::HEIMDALL_SHIM,
Kind::Program,
&shim,
Some("nordisk-heimdall-container 0.2.0".into()),
)
.with(
"heimdall-verify-bundle-verifier.wasm",
Kind::Guest,
&guest,
Some("heimdall-guests-0.2.1".into()),
)
.with(
"heimdall-verify-bundle-verifier.wasm.p7s",
Kind::Guest,
&sig,
Some("heimdall-guests-0.2.1".into()),
);
manifest.check().unwrap();
assert!(manifest.carries_heimdall());
std::fs::write(media.join(m::MANIFEST), toml::to_string(&manifest).unwrap()).unwrap();
std::fs::write(media.join(m::HEIMDALL_SHIM), &shim).unwrap();
std::fs::write(media.join("heimdall-verify-bundle-verifier.wasm"), &guest).unwrap();
std::fs::write(media.join("heimdall-verify-bundle-verifier.wasm.p7s"), &sig).unwrap();
let layout = Layout::under(&tmp.path().join("data"));
let fetch = fetch_from_dir(media.clone());
let first = lay(&layout, &fetch, 0).unwrap();
assert_eq!(first.entries.len(), 6);
let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o7777;
assert_eq!(std::fs::read(layout.heimdall_shim()).unwrap(), shim);
assert_eq!(mode(&layout.heimdall_shim()), 0o755);
let g = layout
.heimdall_guests()
.join("heimdall-verify-bundle-verifier.wasm");
assert_eq!(std::fs::read(&g).unwrap(), guest);
assert_eq!(mode(&g), 0o444);
assert_eq!(
std::fs::read(layout.worker_rootfs_tar()).unwrap(),
std::fs::read(media.join(m::WORKER_ROOTFS)).unwrap(),
"the worker's tar, whole"
);
assert_eq!(mode(&layout.worker_rootfs_tar()), 0o444);
assert!(layout.heimdall_oci().is_dir() && layout.heimdall_work().is_dir());
let again = lay(&layout, &fetch, 0).unwrap();
assert!(again.entries.iter().all(|e| e.kept), "{again:?}");
// A guest changed on the data disk is laid again; so is the worker tar.
std::fs::set_permissions(&g, std::fs::Permissions::from_mode(0o644)).unwrap();
std::fs::write(&g, b"evil").unwrap();
std::fs::set_permissions(
layout.worker_rootfs_tar(),
std::fs::Permissions::from_mode(0o644),
)
.unwrap();
std::fs::write(layout.worker_rootfs_tar(), b"evil").unwrap();
let third = lay(&layout, &fetch, 0).unwrap();
assert!(
third
.entries
.iter()
.any(|e| e.drifted && e.name.ends_with(".wasm")),
"{third:?}"
);
assert_eq!(std::fs::read(&g).unwrap(), guest);
assert_eq!(
std::fs::read(layout.worker_rootfs_tar()).unwrap(),
std::fs::read(media.join(m::WORKER_ROOTFS)).unwrap()
);
// Without the shim, nothing of Heimdall is laid.
let plain = tempfile::tempdir().unwrap();
let pmedia = plain.path().join("media");
std::fs::create_dir_all(&pmedia).unwrap();
assert!(!staged(&pmedia).carries_heimdall());
let pl = Layout::under(&plain.path().join("data"));
lay(&pl, &fetch_from_dir(pmedia), 0).unwrap();
assert!(!pl.worker_rootfs_tar().exists() && !pl.heimdall_guests().exists());
}
}