server/scan-worker/tests/jobs_under_heimdall.rs
//! **Holger's jobs under Heimdall's protected runner, on a real kernel** (lane
//! HOL-HEIMDALL-02): `engine: heimdall` with the real `heimdall-container-shim`
//! (nordisk-heimdall-container 0.2.0 from crates.io, `--features youki`) and the real,
//! static `holger-scan-worker` in an image minted from the medium's rootfs tar
//! (`media::worker_rootfs_tar`), exactly as a box lays it:
//!
//! * a signed bundle is verified by the `verify-bundle` job and its members come back,
//! believed through Heimdall's signature and then Holger's;
//! * a scan (the INGEST walk) runs in the worker and finds the planted bomb;
//! * a bundle with a changed byte is the job's refusal; a job past its wall clock is
//! Holger's timeout, Heimdall's code in the reason;
//! * every result says `heimdall`, Landlock, the fixed seccomp profile and the kernel report.
//!
//! `#[ignore]`d: they need user namespaces, Landlock, seccomp and cgroup v2 with memory,
//! pids and cpu delegated. `scripts/heimdall-kernel-tests.sh` runs them in a privileged
//! rootless podman container (like heimdall's own `scripts/container-tests.sh`), with
//! `HEIMDALL_CONTAINER_SHIM` and `HOLGER_SCAN_WORKER_STATIC` set.
mod common;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use server_lib::job_runner::heimdall_core::{Job, JobId, Nonce, Policy, Verdict};
use server_lib::job_runner::holger_job_runner::worker;
use server_lib::job_runner::holger_sandbox::{EngineKind, HeimdallSettings, SandboxConfig};
use server_lib::job_runner::verify_bundle as vb;
use server_lib::job_runner::{JobDirs, Jobs, SandboxRule};
fn need(var: &str) -> PathBuf {
PathBuf::from(
std::env::var(var)
.unwrap_or_else(|_| panic!("{var} is not set: run scripts/heimdall-kernel-tests.sh")),
)
}
fn cfg(dir: &Path) -> SandboxConfig {
let worker = std::fs::read(need("HOLGER_SCAN_WORKER_STATIC")).unwrap();
let tar = dir.join("holger-scan-worker.rootfs.tar");
std::fs::write(
&tar,
server_lib::job_runner::holger_sandbox::media::worker_rootfs_tar(&worker),
)
.unwrap();
SandboxConfig {
engine: EngineKind::Heimdall,
// The privileged rootless podman the script runs in maps ids 0..65535: as its root,
// a job's root maps to 1.. (Heimdall 0.2.1 host ids), never to 0.
userns_base: 1,
work_dir: Some(dir.join("work").display().to_string()),
heimdall: Some(HeimdallSettings {
image_layout: dir.join("oci").display().to_string(),
rootfs_tar: Some(tar.display().to_string()),
shim: Some(need("HEIMDALL_CONTAINER_SHIM").display().to_string()),
cgroup_parent: std::env::var("HOLGER_HEIMDALL_CGROUP_PARENT")
.unwrap_or_else(|_| "heimdall".into()),
work_dir: Some(dir.join("heimdall").display().to_string()),
fallback_plain: false,
host_id_count: 60000,
..Default::default()
}),
..Default::default()
}
}
fn jobs(dir: &Path) -> Jobs {
let j = Jobs::from_config(SandboxRule::Required, Some(cfg(dir))).expect("Heimdall starts");
assert_eq!(j.fallback(), None, "Heimdall itself, no fallback");
assert_eq!(j.features().runner, "heimdall");
assert!(!j.is_interim());
j
}
fn verify_job(base: &Path, bundle: &[u8], policy: &update_bundle::TrustPolicy) -> (Job, JobDirs) {
let id = JobId::random();
let dirs = JobDirs::new(base, &id).unwrap();
std::fs::write(dirs.input.join(vb::BUNDLE_FILE), bundle).unwrap();
std::fs::write(
dirs.input.join(vb::POLICY_FILE),
serde_json::to_vec(policy).unwrap(),
)
.unwrap();
let params = serde_json::to_value(vb::Params {
time: vb::TimeParam::At {
unix_ms: chrono::Utc::now().timestamp_millis(),
trusted: false,
},
product: None,
model: None,
revision: None,
guest: None,
})
.unwrap();
let job = Job {
id,
nonce: Nonce::random(),
kind: worker(&[vb::PROGRAM]),
input_dir: dirs.input.clone(),
output_dir: dirs.output.clone(),
params,
policy: Policy {
limits: server_lib::job_runner::heimdall_core::Limits {
wall: std::time::Duration::from_secs(60),
memory_bytes: 512 << 20,
..Default::default()
},
..Default::default()
},
};
(job, dirs)
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_bundle_is_verified_in_heimdall_and_its_members_come_back() {
let tmp = tempfile::tempdir().unwrap();
let j = jobs(tmp.path());
let (bundle, policy) = common::signed_bundle(tmp.path());
let good = std::fs::read(&bundle).unwrap();
let (job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy);
let r = j.run(job).expect("a believed result");
assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
assert_eq!(r.runner.runner, "heimdall");
assert!(r.runner.landlock, "Landlock reported from inside the job");
assert!(r.runner.container && r.runner.image_verified_each_start);
let k = r.kernel.as_ref().expect("Heimdall's kernel report");
assert!(
k.seccomp_filter && k.landlock_abi.is_some() && k.user_namespaces,
"{k:?}"
);
assert_eq!(j.kernel(), Some(k));
let (members, manifest, raw) = vb::read_outputs(&dirs.output).unwrap();
assert_eq!(
members.get("payload/firmware.bin").map(Vec::as_slice),
Some(&b"hello"[..])
);
assert!(!manifest.export);
assert_eq!(raw.unwrap(), good, "the bytes that verified come back");
// Heimdall 0.2.1: as root, the job's root is not host root. What it wrote is owned by the
// first id of the range (1 here), not by 0.
{
use std::os::unix::fs::MetadataExt as _;
let me = std::fs::metadata("/proc/self").unwrap().uid();
let owner = std::fs::metadata(dirs.output.join(vb::MEMBERS_FILE))
.unwrap()
.uid();
if me == 0 {
assert_eq!(owner, 1, "a job's root maps to host id 1, not to host root");
} else {
assert_eq!(owner, me, "an ordinary runner's jobs are its own uid");
}
}
let mut bad = good.clone();
let n = bad.len() / 2;
bad[n] ^= 0x40;
let (job, dirs) = verify_job(&tmp.path().join("jobs"), &bad, &policy);
let r = j.run(job).expect("a believed refusal");
assert!(matches!(r.verdict, Verdict::Refused { .. }), "{r:?}");
assert!(std::fs::read_dir(&dirs.output).unwrap().next().is_none());
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_scan_runs_in_heimdall_and_finds_the_planted_bomb() {
use server_lib::sandbox::{JobTemplate, ScanWorker};
use server_lib::scanner::{ScanSubject, ScannerKind};
let tmp = tempfile::tempdir().unwrap();
let j = Arc::new(jobs(tmp.path()));
let mut c = cfg(tmp.path());
c.limits.timeout_secs = 60;
let w = ScanWorker::with_jobs_config(
j,
SandboxRule::Required,
Some(c),
JobTemplate {
format_name: "pip".into(),
kinds: vec![ScannerKind::Inspection],
..Default::default()
},
)
.unwrap();
assert!(w.is_isolated());
let bomb = {
use std::io::Write as _;
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..64 {
e.write_all(&vec![0u8; 1 << 20]).unwrap();
}
e.finish().unwrap()
};
let f = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &bomb));
assert!(
f.has(
holger_inspect::FindingKind::ZipBomb,
holger_inspect::Severity::Block
),
"{:?}",
f.findings
);
let clean = w.inspect(&ScanSubject::new("notes.txt", "raw", b"hello"));
assert_eq!(
clean.verdict(),
holger_inspect::Verdict::Pass,
"{:?}",
clean.findings
);
assert!(w.runs() >= 2 || w.jobs().runs() >= 2);
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_job_past_its_wall_clock_is_holgers_timeout_with_heimdalls_code() {
use server_lib::sandbox::{JobTemplate, ScanWorker};
use server_lib::scanner::{ScanSubject, ScannerKind};
use std::io::Write as _;
let tmp = tempfile::tempdir().unwrap();
let j = Arc::new(jobs(tmp.path()));
let mut c = cfg(tmp.path());
c.limits.timeout_secs = 2;
// The inspector told to let everything through, so only the wall clock stops the walk:
// 32 GiB of zeros as 512 gzip members of 64 MiB (about 32 MiB on the wire).
let loose = holger_inspect::InspectConfig {
limits: holger_inspect::Limits {
max_ratio: u64::MAX / 2,
ratio_floor_bytes: u64::MAX / 2,
max_total_expanded: u64::MAX / 2,
max_entry_bytes: u64::MAX / 2,
..Default::default()
},
..Default::default()
};
let w = ScanWorker::with_jobs_config(
j,
SandboxRule::Required,
Some(c),
JobTemplate {
format_name: "pip".into(),
inspect: loose,
kinds: vec![ScannerKind::Inspection],
..Default::default()
},
)
.unwrap();
let member = {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..64 {
e.write_all(&vec![0u8; 1 << 20]).unwrap();
}
e.finish().unwrap()
};
let outer: Vec<u8> = std::iter::repeat_n(member.as_slice(), 512)
.flatten()
.copied()
.collect();
let f = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &outer));
assert_eq!(
f.verdict(),
holger_inspect::Verdict::Fail,
"{:?}",
f.findings
);
assert!(
f.findings[0].detail.contains("[holger-sandbox-2]")
&& f.findings[0].detail.contains("heimdall-limit-wall")
|| f.findings[0]
.detail
.starts_with("sandbox-timeout: [holger-sandbox-2]"),
"{:?}",
f.findings
);
}
/// Lane HOL-HEIMDALL-04: the released split guest runs inside the worker under Heimdall's
/// protections (wasmtime's JIT under Heimdall's seccomp profile and Landlock), and its
/// members come back as the native ones do.
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_bundle_is_verified_by_the_split_guest_inside_heimdall() {
let tmp = tempfile::tempdir().unwrap();
let j = jobs(tmp.path());
let (bundle, policy) = common::signed_bundle(tmp.path());
let good = std::fs::read(&bundle).unwrap();
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let guests = repo.join(holger_job_runner::guests::DIR);
let (mut job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy);
let pem = std::fs::read(repo.join("holger-recipe/trust/korp-root-ca.pem")).unwrap();
std::fs::write(
dirs.input.join(vb::GUEST_ROOT_FILE),
holger_job_runner::guests::der_of(&pem).unwrap(),
)
.unwrap();
let mut params: vb::Params = serde_json::from_value(job.params.clone()).unwrap();
params.guest = Some(vb::GuestParam { read: 0 });
job.params = serde_json::to_value(¶ms).unwrap();
job.policy.read = vec![guests];
job.policy.limits.memory_bytes = 2 << 30;
job.policy.limits.wall = std::time::Duration::from_secs(120);
let r = j.run(job).expect("a believed result");
assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
assert_eq!(r.runner.runner, "heimdall");
let (members, _, raw) = vb::read_outputs(&dirs.output).unwrap();
assert_eq!(
members.get("payload/firmware.bin").map(Vec::as_slice),
Some(&b"hello"[..])
);
assert_eq!(raw.unwrap(), good);
}
/// HOL-HEIMDALL-06: the probe door looks into Heimdall's runner: the worker's `probe` as one
/// job, from inside: no network, no capabilities, no new privileges, a seccomp filter.
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn the_probe_looks_into_heimdalls_runner() {
use server_lib::sandbox::{JobTemplate, ScanWorker};
let tmp = tempfile::tempdir().unwrap();
let j = Arc::new(jobs(tmp.path()));
let w = ScanWorker::with_jobs_config(
j,
SandboxRule::Required,
Some(cfg(tmp.path())),
JobTemplate::default(),
)
.unwrap();
let v = w
.probe()
.expect("the probe runs as a job of Heimdall's runner");
assert!(
v["engine"].as_str().is_some_and(|e| e.contains("Heimdall")),
"{v}"
);
assert!(
v["connect_public"]
.as_str()
.is_some_and(|s| s.starts_with("refused")),
"no network inside: {v}"
);
assert_eq!(v["no_new_privs"], "1", "{v}");
assert_eq!(v["cap_eff"], "0000000000000000", "{v}");
assert_ne!(v["seccomp"], "0", "a seccomp filter: {v}");
}