nordisk/heimdall
crates/heimdall-wasm/tests/good_guest.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Spec §11 Wasm: a good input is accepted, a tampered input is refused with
//! the guest's reason, a malformed input traps and the host only refuses.
//! Heimdall is a defensive sandbox; these guests exercise its own host.
mod common;
use common::*;
use heimdall_wasm::{codes, RunLimits, Verdict};
use sha2::{Digest, Sha256};
#[test]
fn a_good_input_is_accepted_with_the_guest_note_and_its_file() {
let kit = Kit::new("good");
let g = kit.guest("good", &good_wat());
let o = g.run(b"hello bundle", &none(), &RunLimits::default());
assert_eq!(
o.verdict,
Verdict::Accepted {
note: "accepted".into()
}
);
assert_eq!(o.files.len(), 1);
assert_eq!(o.files[0].name, "report.txt");
assert_eq!(o.files[0].bytes, b"hello bundle");
assert!(o.fuel_used > 0, "fuel was not metered");
}
#[test]
fn every_emitted_file_carries_the_sha256_of_its_bytes() {
let kit = Kit::new("sha");
let g = kit.guest("good", &good_wat());
let o = g.run(b"abc", &none(), &RunLimits::default());
let want: [u8; 32] = Sha256::digest(b"abc").into();
assert_eq!(o.files[0].sha256.0, want);
assert_eq!(
o.files[0].entry(),
heimdall_core::Output {
name: "report.txt".into(),
sha256: o.files[0].sha256,
bytes: 3
}
);
}
#[test]
fn a_tampered_input_is_refused_with_the_guests_own_reason() {
let kit = Kit::new("tampered");
let g = kit.guest("good", &good_wat());
let o = g.run(b"Tampered", &none(), &RunLimits::default());
assert_eq!(
o.verdict,
Verdict::Refused {
code: codes::GUEST_REFUSED,
reason: "input signature mismatch".into()
}
);
assert_eq!(
o.verdict.to_string(),
"refused: input signature mismatch [heimdall-wasm-1]"
);
}
#[test]
fn a_malformed_input_traps_the_guest_and_the_host_only_refuses() {
let kit = Kit::new("malformed");
let g = kit.guest("good", &good_wat());
let o = g.run(b"Malformed", &none(), &RunLimits::default());
assert_trapped(&o, "unreachable");
assert!(
o.verdict
.to_string()
.starts_with("refused: guest trapped ("),
"{}",
o.verdict
);
}
#[test]
fn the_same_guest_runs_again_cleanly_after_a_trap() {
let kit = Kit::new("again");
let g = kit.guest("good", &good_wat());
assert!(!g
.run(b"M", &none(), &RunLimits::default())
.verdict
.is_accepted());
assert!(g
.run(b"fine", &none(), &RunLimits::default())
.verdict
.is_accepted());
}
#[test]
fn an_empty_input_is_a_valid_input() {
let kit = Kit::new("empty");
let g = kit.guest("good", &good_wat());
let o = g.run(b"", &none(), &RunLimits::default());
assert!(o.verdict.is_accepted(), "{}", o.verdict);
assert_eq!(o.files[0].bytes, b"");
}
#[test]
fn the_same_input_gives_the_same_outcome_and_the_same_fuel() {
let kit = Kit::new("determinism");
let g = kit.guest("good", &good_wat());
let a = g.run(b"deterministic", &none(), &RunLimits::default());
let b = g.run(b"deterministic", &none(), &RunLimits::default());
assert_eq!(a, b);
}
#[test]
fn params_reach_the_guest_in_order() {
let kit = Kit::new("params");
// Note = the first param's value, or "none".
let g = kit.guest(
"params",
&Wat::new(
r#"(if (i32.eqz (local.get $params_len)) (then (return (call $ok (i32.const 16) (i32.const 4)))))
(call $ok (i32.load offset=8 (local.get $params)) (i32.load offset=12 (local.get $params)))"#,
)
.extra(r#"(data (i32.const 16) "none")"#)
.text(),
);
// Params are sorted by key (heimdall-core's BTreeMap): "mode" comes first.
let p = params(&[("x", "y"), ("mode", "strict")]);
assert_eq!(
g.run(b"", &p, &RunLimits::default()).verdict,
Verdict::Accepted {
note: "strict".into()
}
);
assert_eq!(
g.run(b"", &none(), &RunLimits::default()).verdict,
Verdict::Accepted {
note: "none".into()
}
);
}
#[test]
fn the_input_reaches_the_guest_byte_for_byte() {
let kit = Kit::new("echo");
let g = kit.guest(
"echo",
&Wat::new("(call $ok (local.get $in) (local.get $in_len))").text(),
);
let o = g.run("åäö ✓".as_bytes(), &none(), &RunLimits::default());
assert_eq!(
o.verdict,
Verdict::Accepted {
note: "åäö ✓".into()
}
);
}
#[test]
fn a_guest_refusal_keeps_the_files_it_emitted_as_its_report() {
let kit = Kit::new("refusal-report");
let g = kit.guest(
"report",
&Wat::new(
r#"(call $emit (i32.const 16) (i32.const 10) (i32.const 32) (i32.const 3))
(call $refuse (i32.const 32) (i32.const 3))"#,
)
.extra(r#"(data (i32.const 16) "why.report") (data (i32.const 32) "bad")"#)
.text(),
);
let o = g.run(b"", &none(), &RunLimits::default());
assert_eq!(
o.verdict,
Verdict::Refused {
code: codes::GUEST_REFUSED,
reason: "bad".into()
}
);
assert_eq!(o.files.len(), 1);
assert_eq!(o.files[0].name, "why.report");
}
#[test]
fn one_guest_runs_on_many_threads_at_once() {
let kit = Kit::new("threads");
let g = std::sync::Arc::new(kit.guest("good", &good_wat()));
let hs: Vec<_> = (0..4)
.map(|i| {
let g = g.clone();
std::thread::spawn(move || {
g.run(format!("in{i}").as_bytes(), &none(), &RunLimits::default())
})
})
.collect();
for (i, h) in hs.into_iter().enumerate() {
let o = h.join().unwrap();
assert!(o.verdict.is_accepted(), "{}", o.verdict);
assert_eq!(o.files[0].bytes, format!("in{i}").as_bytes());
}
}
#[test]
fn a_loaded_guest_reports_its_digest_and_signer() {
let kit = Kit::new("census");
let bytes = component(&good_wat());
let path = kit.place("good", &bytes);
let g = kit.host().load(&path).unwrap();
assert_eq!(g.digest_hex(), blake3::hash(&bytes).to_hex().as_str());
assert_eq!(g.signer(), "heimdall-test-release");
}
#[test]
fn a_guest_runs_under_heimdall_core_default_limits() {
// The limits a job policy carries by default must admit an ordinary
// component guest (three core instances: module, shim, fixup).
let kit = Kit::new("core-defaults");
let g = kit.guest("good", &good_wat());
let limits = RunLimits::from(&heimdall_core::Limits::default());
let o = g.run(b"x", &none(), &limits);
assert!(o.verdict.is_accepted(), "{}", o.verdict);
}
#[test]
fn the_core_limits_map_field_by_field() {
let core = heimdall_core::Limits {
fuel: Some(7),
wall: std::time::Duration::from_millis(1500),
memory_bytes: 1 << 20,
cpus: 1.0,
pids: 8,
table_elements: 11,
instances: 5,
tables: 2,
memories: 1,
stack_bytes: 1 << 16,
max_output_files: 3,
max_output_file_bytes: 100,
max_output_total_bytes: 200,
};
let r = RunLimits::from(&core);
assert_eq!(r.fuel, 7);
assert_eq!(r.timeout, std::time::Duration::from_millis(1500));
assert_eq!(
(
r.memory_bytes,
r.table_elements,
r.instances,
r.tables,
r.memories
),
(1 << 20, 11, 5, 2, 1)
);
assert_eq!(
(r.max_files, r.max_file_bytes, r.max_emit_bytes),
(3, 100, 200)
);
let unmetered = RunLimits::from(&heimdall_core::Limits { fuel: None, ..core });
assert_eq!(
unmetered.fuel,
u64::MAX,
"no fuel metering means only the clock stops it"
);
let cfg = heimdall_wasm::HostConfig::for_policy([std::path::PathBuf::from("r.der")], &core);
assert_eq!(cfg.stack_bytes, 1 << 16);
}