crates/heimdall-cli/tests/genprof.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! `heimdall run --job wasm` under the GENERATED heimdall-wasm-host seccomp
//! profile (lane genprof): the guest runs in a worker process whose kernel
//! filter is the checked-in profile; the parent signs. Heimdall is a
//! defensive sandbox; the only call these tests make outside the profile is
//! the test hook's own socket (feature `test-hooks`), which the filter must
//! kill.
//!
//! The profiles are generated from the RELEASE build, so these tests build
//! their own release `heimdall` (with `test-hooks`) once, in
//! `CARGO_TARGET_TMPDIR`, or take `HEIMDALL_GENPROF_BIN`.
#[path = "../../heimdall-wasm/tests/common/mod.rs"]
mod common;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::OnceLock;
use common::{component, good_wat, Kit, Wat};
use heimdall_core::{codes, Protection, SignedResult, Verdict};
use sha2::{Digest, Sha256};
const OK: i32 = 0;
const USAGE: i32 = 2;
const HOST: i32 = 3;
const REFUSED: i32 = 4;
/// The release `heimdall` with `test-hooks`, built once per test binary.
fn release_bin() -> &'static Path {
static B: OnceLock<PathBuf> = OnceLock::new();
B.get_or_init(|| {
if let Some(p) = std::env::var_os("HEIMDALL_GENPROF_BIN") {
return PathBuf::from(p);
}
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("heimdall-genprof");
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let st = Command::new(cargo)
.args([
"build",
"--locked",
"--release",
"-p",
"nordisk-heimdall-cli",
"--bin",
"heimdall",
"--features",
"test-hooks",
"--manifest-path",
])
.arg(repo.join("Cargo.toml"))
.arg("--target-dir")
.arg(&target)
.env_remove("RUSTFLAGS")
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.status()
.expect("cargo could not be started");
assert!(st.success(), "building the release heimdall failed");
target.join("release/heimdall")
})
}
struct Job {
kit: Kit,
root: PathBuf,
module: PathBuf,
}
impl Job {
fn new(name: &str, core_wat: &str, input: &[u8]) -> Job {
let kit = Kit::new(&format!("genprof-{name}"));
let root = kit.dir.join("job");
std::fs::create_dir_all(root.join("in")).unwrap();
std::fs::write(root.join("in/input.bin"), input).unwrap();
let module = kit.place(name, &component(core_wat));
Job { kit, root, module }
}
fn good(name: &str, input: &[u8]) -> Job {
Self::new(name, &good_wat(), input)
}
fn out(&self) -> PathBuf {
self.root.join("out")
}
fn cmd(&self, extra: &[&str]) -> Command {
let mut c = Command::new(release_bin());
c.args([
"run",
"--job",
"wasm",
"--module",
self.module.to_str().unwrap(),
"--trust-root",
self.kit.root.to_str().unwrap(),
"--in",
self.root.join("in").to_str().unwrap(),
"--out",
self.out().to_str().unwrap(),
"--pin-out",
self.root.join("key.pub").to_str().unwrap(),
])
.args(extra)
.env_remove("HEIMDALL_SECCOMP")
.env_remove("HEIMDALL_TEST_WORKER_CALL");
c
}
fn run(&self, extra: &[&str]) -> Output {
self.cmd(extra).output().unwrap()
}
fn verify(&self) -> Output {
Command::new(release_bin())
.args([
"verify-result",
self.out().join("result.signed").to_str().unwrap(),
"--key",
self.root.join("key.pub").to_str().unwrap(),
])
.output()
.unwrap()
}
fn signed(&self) -> SignedResult {
SignedResult::from_bytes(&std::fs::read(self.out().join("result.signed")).unwrap()).unwrap()
}
fn files(&self) -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(self.out().join("files"))
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
v.sort();
v
}
}
fn code(o: &Output) -> i32 {
o.status
.code()
.expect("the heimdall parent is never killed")
}
fn err(o: &Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
fn out(o: &Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
/// `sha256:<hex>` of this build's checked-in wasm-host profile (OCI JSON).
fn builtin_digest() -> String {
let libc = if cfg!(target_env = "musl") {
heimdall_seccomp::Libc::Musl
} else {
heimdall_seccomp::Libc::Glibc
};
let e = heimdall_profiles::ALL
.iter()
.find(|e| {
e.profile.workload == "heimdall-wasm-host"
&& Some(e.profile.arch) == heimdall_seccomp::Arch::native()
&& e.profile.libc == libc
})
.expect("a checked-in wasm-host profile for this build");
let h: [u8; 32] = Sha256::digest(e.oci_json.as_bytes()).into();
format!("sha256:{}", heimdall_core::hex::encode(&h))
}
#[test]
fn by_default_a_wasm_job_runs_under_the_generated_profile_and_the_signed_result_names_its_digest() {
let j = Job::good("default", b"hello generated");
let r = j.run(&[]);
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
let d = builtin_digest();
assert!(
out(&r).contains(&format!("seccomp: generated {d}")),
"{}",
out(&r)
);
let s = j.signed();
assert!(s.result.verdict.is_accepted());
assert_eq!(
s.result.runner.seccomp,
Protection::Generated { profile_digest: d }
);
assert!(s.result.runner.wasm && !s.result.runner.container);
assert_eq!(j.files(), vec!["report.txt".to_string()]);
assert_eq!(code(&j.verify()), OK, "{}", err(&j.verify()));
}
#[test]
fn a_guest_refusal_and_a_guest_trap_under_the_generated_profile_are_signed_refusals_as_before() {
let j = Job::good("tampered", b"Tampered bundle");
let r = j.run(&[]);
assert_eq!(code(&r), REFUSED, "{}", err(&r));
match &j.signed().result.verdict {
Verdict::Refused { reason, .. } => assert_eq!(reason, "input signature mismatch"),
v => panic!("{v:?}"),
}
let j = Job::good("malformed", b"Malformed");
let r = j.run(&[]);
assert_eq!(code(&r), REFUSED, "{}", err(&r));
let s = j.signed();
assert!(
s.result
.verdict
.code()
.unwrap()
.starts_with("heimdall-wasm-"),
"{:?}",
s.result.verdict
);
assert_eq!(s.result.exit.signal, None);
assert!(j.files().is_empty());
assert_eq!(code(&j.verify()), REFUSED);
}
#[test]
fn an_infinite_loop_under_the_generated_profile_is_stopped_by_fuel_not_by_the_filter() {
let j = Job::new(
"spin",
&Wat::new("(loop $l (br $l)) (i32.const 0)").text(),
b"x",
);
let p = j.root.join("policy.toml");
std::fs::write(
&p,
"version = 1\n[limits]\nfuel = 1000000\nwall_time_ms = 60000\n",
)
.unwrap();
let r = j.run(&["--policy", p.to_str().unwrap()]);
assert_eq!(code(&r), REFUSED, "{}", err(&r));
let s = j.signed();
assert!(
s.result
.verdict
.code()
.unwrap()
.starts_with("heimdall-wasm-"),
"{:?}",
s.result.verdict
);
assert_ne!(s.result.verdict.code(), Some(codes::SECCOMP_KILL));
assert_eq!(s.result.exit.signal, None);
}
#[test]
fn a_call_outside_the_profile_kills_the_worker_with_sigsys_and_the_parent_signs_a_seccomp_refusal()
{
let j = Job::good("sigsys", b"hello");
let r = j
.cmd(&[])
.env("HEIMDALL_TEST_WORKER_CALL", "socket")
.output()
.unwrap();
assert_eq!(code(&r), REFUSED, "{}", err(&r));
let s = j.signed();
assert_eq!(
s.result.verdict.code(),
Some(codes::SECCOMP_KILL),
"{:?}",
s.result.verdict
);
assert_eq!(s.result.exit.signal, Some(libc::SIGSYS));
assert!(s.result.outputs.is_empty());
assert!(j.files().is_empty());
assert_eq!(
s.result.runner.seccomp,
Protection::Generated {
profile_digest: builtin_digest()
}
);
// The refusal verifies under the pinned key like any result.
assert_eq!(code(&j.verify()), REFUSED);
}
#[test]
fn the_same_hook_without_the_filter_is_not_killed_so_the_filter_did_the_killing() {
let j = Job::good("hook-none", b"hello");
let r = j
.cmd(&["--seccomp", "none"])
.env("HEIMDALL_TEST_WORKER_CALL", "socket")
.output()
.unwrap();
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
}
#[test]
fn seccomp_none_runs_in_process_and_the_result_says_none() {
let j = Job::good("none", b"x");
let r = j.run(&["--seccomp", "none"]);
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
assert!(out(&r).contains("seccomp: none"));
assert_eq!(j.signed().result.runner.seccomp, Protection::None);
let j = Job::good("none-env", b"x");
let r = j.cmd(&[]).env("HEIMDALL_SECCOMP", "none").output().unwrap();
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
assert_eq!(j.signed().result.runner.seccomp, Protection::None);
// The flag wins over the environment.
let j = Job::good("flag-wins", b"x");
let r = j
.cmd(&["--seccomp", "generated"])
.env("HEIMDALL_SECCOMP", "none")
.output()
.unwrap();
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
assert!(matches!(
j.signed().result.runner.seccomp,
Protection::Generated { .. }
));
}
#[test]
fn a_bad_heimdall_seccomp_value_is_a_usage_error() {
let j = Job::good("badenv", b"x");
let r = j.cmd(&[]).env("HEIMDALL_SECCOMP", "off").output().unwrap();
assert_eq!(code(&r), USAGE, "{}", err(&r));
assert!(!j.out().exists());
}
#[test]
fn a_pinned_profile_digest_that_differs_refuses_the_job_and_nothing_runs() {
let j = Job::good("pin-bad", b"x");
let r = j.run(&["--seccomp-sha256", &format!("sha256:{}", "00".repeat(32))]);
assert_eq!(code(&r), HOST, "{}", err(&r));
assert!(err(&r).contains("digest mismatch"), "{}", err(&r));
assert!(!j.out().exists());
let j = Job::good("pin-ok", b"x");
let r = j.run(&["--seccomp-sha256", &builtin_digest()]);
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
}
#[test]
fn a_guest_that_fails_to_load_in_the_worker_is_a_host_error_with_nothing_signed() {
let j = Job::good("badpin", b"x");
let r = j.run(&["--pin-blake3", &"00".repeat(32)]);
assert_eq!(code(&r), HOST, "{}", err(&r));
assert!(!j.out().join("result.signed").exists());
}
#[test]
fn caller_chosen_job_id_and_nonce_are_the_parents_not_the_workers() {
let j = Job::good("ticket", b"x");
let id = "00112233445566778899aabbccddeeff";
let nonce = "cd".repeat(32);
let r = j.run(&["--job-id", id, "--nonce", &nonce]);
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
let s = j.signed();
assert_eq!(s.result.job_id.to_hex(), id);
assert_eq!(s.result.nonce.to_hex(), nonce);
}
#[test]
fn a_bare_worker_flag_never_signs_anything() {
let j = Job::good("bare-worker", b"x");
let r = j.run(&["--heimdall-worker"]);
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
assert!(!j.out().join("result.signed").exists());
assert!(!j.root.join("key.pub").exists());
}
#[test]
fn a_debug_build_runs_without_a_filter_by_default_and_refuses_an_explicit_generated() {
if !cfg!(debug_assertions) {
return; // CARGO_BIN_EXE_heimdall is the release build here
}
let j = Job::good("debug-default", b"x");
// The same arguments, run by the debug binary.
let args: Vec<_> = j.cmd(&[]).get_args().map(|a| a.to_owned()).collect();
let r = Command::new(env!("CARGO_BIN_EXE_heimdall"))
.args(&args)
.env_remove("HEIMDALL_SECCOMP")
.output()
.unwrap();
assert_eq!(code(&r), OK, "{}{}", err(&r), out(&r));
assert_eq!(j.signed().result.runner.seccomp, Protection::None);
let j = Job::good("debug-generated", b"x");
let args: Vec<_> = j
.cmd(&["--seccomp", "generated"])
.get_args()
.map(|a| a.to_owned())
.collect();
let r = Command::new(env!("CARGO_BIN_EXE_heimdall"))
.args(&args)
.output()
.unwrap();
assert_eq!(code(&r), HOST, "{}", err(&r));
assert!(err(&r).contains("debug build"), "{}", err(&r));
assert!(!j.out().exists());
}