server/cli/src/sandbox_boot.rs
//! **The scan sandbox laid at boot** (lane HOLGER-MEDIA, 2026-10-23): the
//! appliance's half of `server_lib::sandbox::media`.
//!
//! On a box korp-start booted, the sandbox entries ride the running slot's app
//! archive (the same signed `.znippy` the server was `fexecve`'d out of). This
//! module reads that archive the way korp-start does (the slot from the ESP's
//! setting, `korp/app.len`, the raw partition staged into a memfd), hands each
//! entry to the lay (sha256-checked against the manifest), mounts cgroup v2 for
//! the limits, and starts clamd in its sandbox, supervised. All of it before the
//! intake chain opens, so `rules: (sandbox: required)` holds from the first scan.
//!
//! Elsewhere (a container, a developer's box, a test), `HOLGER_SANDBOX_MEDIA_DIR`
//! names a directory of entries (`cargo xtask scan-worker stage`); on a box korp
//! booted the variable is ignored, with a warning (NCSC P1, lane HOL-NCSC). With neither,
//! nothing is laid and the intake refuses every scan by name, as before.
use std::path::{Path, PathBuf};
use std::time::Duration;
use holger_sandbox::crun::{Kernel, Runtime, State};
use holger_sandbox::media::Layout;
use holger_sandbox::Refusal;
use server_lib::sandbox::{clamd, media};
/// cgroup v2's mount point (the kernel's own; `holger_sandbox::crun::CGROUP_ROOT`).
const CGROUP_ROOT: &str = holger_sandbox::crun::CGROUP_ROOT;
/// Where the entries come from this boot.
enum Source {
/// A directory of entries (`HOLGER_SANDBOX_MEDIA_DIR`).
Dir(PathBuf),
/// The running slot's archive, staged into a memfd (the file keeps it alive).
Archive(PathBuf, std::fs::File),
/// The live medium's archive at a path (`korp.znippy=/app.znippy`).
File(PathBuf),
}
/// **`HOLGER_SANDBOX_MEDIA_DIR`, unless korp booted this box** (NCSC P1, lane HOL-NCSC): on
/// a box korp-start booted the sandbox comes from the running slot's signed app archive only,
/// and the variable is ignored, with a warning that names it. Elsewhere it names the entries.
fn media_dir(env: Option<std::ffi::OsString>, korp_booted: bool) -> Option<PathBuf> {
let d = env?;
if korp_booted {
log::warn!(
"scan sandbox: {}={} is IGNORED: korp-start booted this box, so the sandbox is laid from the running slot's signed app archive only",
media::MEDIA_DIR_ENV,
d.to_string_lossy()
);
return None;
}
Some(PathBuf::from(d))
}
fn source() -> Result<Option<Source>, Refusal> {
let korp = appliance_pid1::net::booted_by_korp_start();
if let Some(d) = media_dir(std::env::var_os(media::MEDIA_DIR_ENV), korp) {
return Ok(Some(Source::Dir(d)));
}
if !korp {
return Ok(None);
}
let selector = korp_installer::cmdline::effective()
.znippy
.unwrap_or_else(|| "/system/app.znippy".into());
match korp_installer::datahome::partition_selector(&selector) {
Some(gpt_name) => {
let esp = korp_installer::live::Esp::boot();
let setting = esp
.read_setting()
.map_err(|e| Refusal::Io(format!("the running slot: {e}")))?;
let slot = setting.active;
let gpt_name = if gpt_name == korp_installer::live::Slot::A.partition() {
slot.partition()
} else {
gpt_name
};
let len_at = esp.records(slot).len;
let app_len: u64 = std::fs::read_to_string(&len_at)
.map_err(|e| Refusal::Io(format!("{}: {e}", len_at.display())))?
.trim()
.parse()
.map_err(|e| Refusal::Io(format!("{}: not a length ({e})", len_at.display())))?;
let mut serial = korp_installer::serial::Serial::in_memory();
let (path, keep) =
korp_installer::datahome::stage_partition_archive(gpt_name, app_len, &mut serial)
.map_err(|()| {
Refusal::Io(format!(
"the app partition {gpt_name:?} could not be staged into memory"
))
})?;
Ok(Some(Source::Archive(path, keep)))
}
None => {
let p = PathBuf::from(selector);
if p.is_file() {
Ok(Some(Source::File(p)))
} else {
Ok(None)
}
}
}
}
/// Mount cgroup v2 at its root when the kernel has not (an appliance boot).
fn mount_cgroup2() -> Result<(), Refusal> {
if Path::new(CGROUP_ROOT).join("cgroup.controllers").exists() {
return Ok(());
}
let fs = appliance_pid1::mounts::PseudoFs {
source: "cgroup2",
target: CGROUP_ROOT,
fstype: "cgroup2",
dir_mode: 0o755,
flags: libc::MS_NOSUID | libc::MS_NOEXEC | libc::MS_NODEV,
};
appliance_pid1::mounts::mount_one(&fs).map_err(|errno| Refusal::NoEngine(format!("cgroup2 would not mount at {CGROUP_ROOT} (errno {errno}); the limits need CONFIG_CGROUPS")))
}
/// **An exec-capable view of the sandbox at its own path.** The data volume is
/// mounted `nosuid,nodev,noexec` (korp lays it so, rightly: a site's data is
/// never code), and the runtime, the worker's and ClamAV's rootfs live under it;
/// MEASURED on the laid box: `crun --version: Permission denied`. So the sandbox
/// directory — verified content only: the runtime and the rootfs the manifest's
/// sha256 vouched for — is bind-mounted onto itself and remounted `exec`, still
/// `nosuid,nodev`; every other byte of /data stays noexec, and every path in the
/// declaration stays as it is. Idempotent across a live re-exec (the mount is
/// found, not stacked).
fn exec_bind(dir: &Path) -> Result<(), Refusal> {
let mountinfo = std::fs::read_to_string("/proc/self/mountinfo")
.map_err(|e| Refusal::Io(format!("/proc/self/mountinfo: {e}")))?;
match media::mount_noexec_for(&mountinfo, dir) {
Some(false) | None => return Ok(()),
Some(true) => {}
}
let c = std::ffi::CString::new(dir.as_os_str().as_encoded_bytes())
.map_err(|_| Refusal::Io(format!("{}: not a C path", dir.display())))?;
// SAFETY: both pointers are NUL-terminated C strings owned by `c`, which
// outlives both calls; `mount(2)` only reads through them; the fstype and
// data pointers are null (a bind and a remount take neither). The flags are
// libc's own MS_* constants.
let bound = unsafe {
libc::mount(
c.as_ptr(),
c.as_ptr(),
std::ptr::null(),
libc::MS_BIND,
std::ptr::null(),
)
};
if bound != 0 {
return Err(Refusal::Io(format!(
"bind {} onto itself: {}",
dir.display(),
std::io::Error::last_os_error()
)));
}
let flags = libc::MS_REMOUNT | libc::MS_BIND | libc::MS_NOSUID | libc::MS_NODEV;
// SAFETY: as above; a remount of the bind just made, exec allowed.
let remounted = unsafe {
libc::mount(
std::ptr::null(),
c.as_ptr(),
std::ptr::null(),
flags,
std::ptr::null(),
)
};
if remounted != 0 {
return Err(Refusal::Io(format!(
"remount {} exec,nosuid,nodev: {}",
dir.display(),
std::io::Error::last_os_error()
)));
}
log::info!("scan sandbox: {} remounted exec,nosuid,nodev over the noexec data volume (verified content only)", dir.display());
Ok(())
}
/// The containers' user namespace base: the engine's default as root, none as
/// an ordinary user (a developer's box cannot shift owners).
pub fn userns_base() -> u32 {
// SAFETY: `geteuid(2)` takes nothing, touches no memory and cannot fail.
if unsafe { libc::geteuid() } == 0 {
holger_sandbox::crun::DEFAULT_USERNS_BASE
} else {
0
}
}
/// **The boot step.** `Ok(None)`: nothing to lay here. `Ok(Some)`: laid (or kept)
/// and clamd answering in its sandbox. `Err`: refused by name; the server serves
/// and the intake refuses every scan.
pub fn boot(data_root: &Path) -> Result<Option<media::Laid>, Refusal> {
let Some(src) = source()? else {
return Ok(None);
};
// This process is PID 1 on the appliance: its reaper leaves claimed children to
// the engines (appliance_pid1::claim), or every scan's status could be lost.
holger_sandbox::set_child_claim(|pid| Box::new(appliance_pid1::claim(pid)));
let layout = Layout::under(data_root);
let base = userns_base();
let laid = match &src {
Source::Dir(d) => media::lay(&layout, &media::fetch_from_dir(d.clone()), base)?,
Source::Archive(p, _keep) => lay_from_archive(&layout, p, base)?,
Source::File(p) => lay_from_archive(&layout, p, base)?,
};
drop(src);
if appliance_pid1::net::booted_by_korp_start() {
mount_cgroup2()?;
exec_bind(&layout.dir)?;
}
let kernel = Kernel::probe()?;
holger_sandbox::crun::cgroup_ready(Path::new(CGROUP_ROOT))?;
let rt = Runtime {
program: layout.crun().display().to_string(),
state: State {
dir: layout.state(),
},
};
rt.version()?;
let spec = clamd::spec(&layout, base, kernel.ipc);
let c = clamd::Clamd::start(rt, spec, layout.clamd_socket(), clamd::START_BUDGET)?;
log::info!(
"clamd answers in its sandbox at {} (crun, uid {}, no network)",
c.socket.display(),
clamd::CLAMD_UID
);
// A28 M18, stays a task on the server's runtime: clamd's supervisor, which says and restarts clamd itself.
tokio::spawn(clamd::supervise(c, Duration::from_secs(2)));
Ok(Some(laid))
}
#[cfg(feature = "sign")]
fn lay_from_archive(layout: &Layout, archive: &Path, base: u32) -> Result<media::Laid, Refusal> {
media::lay(
layout,
&media::fetch_from_archive(archive.to_path_buf()),
base,
)
}
#[cfg(not(feature = "sign"))]
fn lay_from_archive(_layout: &Layout, archive: &Path, _base: u32) -> Result<media::Laid, Refusal> {
Err(Refusal::NoEngine(format!(
"this build reads no archive ({}): it was built without `sign`",
archive.display()
)))
}
/// **The release's Sigstore trust root, seeded from the medium** (lane HOL-OCI-EVIDENCE): the
/// signed bundle and the policy that pins its signer ride beside the sandbox entries (the same
/// signed app archive, or `HOLGER_SANDBOX_MEDIA_DIR`); they are laid under
/// `<data_root>/holger-trust` before the intake chain opens (`server_lib::sigstore_trust::seed`:
/// a same-or-later root an import installed is kept, the floor rises). `Ok(None)`: the medium
/// carries none, and the box verifies no keyless signature until a signed bundle is imported.
pub fn seed_trust(data_root: &Path) -> Result<Option<server_lib::sigstore_trust::Seeded>, String> {
match source().map_err(|e| e.to_string())? {
Some(src) => seed_trust_from(&src, data_root),
None => Ok(None),
}
}
fn seed_trust_from(
src: &Source,
data_root: &Path,
) -> Result<Option<server_lib::sigstore_trust::Seeded>, String> {
let fetch: Box<dyn Fn(&str) -> Option<Vec<u8>>> = match src {
Source::Dir(d) => {
let f = media::fetch_from_dir(d.clone());
Box::new(move |n: &str| f(n).ok())
}
Source::Archive(p, _) | Source::File(p) => archive_fetch(p),
};
server_lib::sigstore_trust::seed(data_root, &*fetch)
}
#[cfg(feature = "sign")]
fn archive_fetch(p: &Path) -> Box<dyn Fn(&str) -> Option<Vec<u8>>> {
let f = media::fetch_from_archive(p.to_path_buf());
Box::new(move |n: &str| f(n).ok())
}
#[cfg(not(feature = "sign"))]
fn archive_fetch(_p: &Path) -> Box<dyn Fn(&str) -> Option<Vec<u8>>> {
Box::new(|_: &str| None)
}
#[cfg(test)]
mod tests {
use super::*;
/// The medium's two trust entries (a directory of entries here) are seeded under the data root.
#[test]
fn the_medium_seeds_the_trust_root_under_the_data_root() {
let medium = tempfile::tempdir().unwrap();
let signer = tempfile::tempdir().unwrap();
let root = std::fs::read(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../provenance/tests/fixtures/sigstore/trusted_root.json"
))
.unwrap();
let policy = server_lib::sigstore_trust::make(
&root,
signer.path(),
&medium.path().join(server_lib::sigstore_trust::BUNDLE_FILE),
7,
30,
None,
)
.unwrap();
std::fs::write(
medium.path().join(server_lib::sigstore_trust::POLICY_FILE),
policy.to_ron().unwrap(),
)
.unwrap();
let data = tempfile::tempdir().unwrap();
let s = seed_trust_from(&Source::Dir(medium.path().to_path_buf()), data.path())
.unwrap()
.expect("seeded");
assert_eq!((s.sequence, s.kept), (7, false));
assert!(server_lib::sigstore_trust::bundle_path(data.path()).is_file());
assert!(server_lib::sigstore_trust::policy_path(data.path()).is_file());
// What the seed wrote is the medium's, not the box's (lane HOL-REBUILT-SYNC-KEY): a box
// laid again after its main died stays fresh, so its boot hold holds it for its recharge.
assert_eq!(
server_lib::twin::held_state(data.path()),
None,
"the seeded trust root is no history"
);
// A medium without them seeds nothing.
let empty = tempfile::tempdir().unwrap();
assert!(
seed_trust_from(&Source::Dir(empty.path().to_path_buf()), data.path())
.unwrap()
.is_none()
);
}
#[test]
fn the_media_dir_variable_is_ignored_on_a_box_korp_booted() {
assert_eq!(
media_dir(Some("/tmp/entries".into()), false),
Some(PathBuf::from("/tmp/entries")),
"a developer's box, a container, a test"
);
assert_eq!(
media_dir(Some("/tmp/entries".into()), true),
None,
"the appliance lays from its signed archive only"
);
assert_eq!(media_dir(None, true), None);
assert_eq!(media_dir(None, false), None);
}
}