// SPDX-License-Identifier: AGPL-3.0-only
//! Heimdall is a defensive sandbox. Fuzz the wasm host's emit-file path: one
//! signed guest runs a fuzzer-written script of emit-file calls (arbitrary
//! name bytes, arbitrary sizes, traps and refusals mid-output) under small
//! limits. The outcome must match a model: every emitted name is one plain
//! file name accepted by heimdall-core too, no name twice, every count and
//! size limit holds, a trap or a limit hands on no files, and no run ends in
//! a host fault.
#![no_main]
use libfuzzer_sys::fuzz_target;
#[path = "../common/wasm_kit.rs"]
mod wasm_kit;
use heimdall_core::Output;
use heimdall_wasm::{check_file_name, codes, Guest, RunLimits, Verdict};
use std::collections::BTreeSet;
use std::sync::OnceLock;
use std::time::Duration;
fn guest() -> &'static Guest {
static G: OnceLock<Guest> = OnceLock::new();
G.get_or_init(|| {
let kit = wasm_kit::kit();
let path = kit.place_signed("emit", &wasm_kit::component(wasm_kit::EMIT_SCRIPT_WAT));
kit.host.load(&path).expect("emit-script guest loads")
})
}
fn limits() -> RunLimits {
RunLimits {
fuel: 50_000_000,
timeout: Duration::from_secs(5),
max_files: 8,
max_file_bytes: 4096,
max_emit_bytes: 12_288,
..RunLimits::default()
}
}
/// What the run should give, when the script stays inside the input.
enum Expect {
Accepted(Vec<(String, usize)>),
GuestRefused(Vec<(String, usize)>),
Stopped,
Unknown,
}
fn model(script: &[u8], l: &RunLimits) -> Expect {
let mut files = Vec::new();
let mut seen = BTreeSet::new();
let mut total = 0usize;
let mut p = 0usize;
while p < script.len() {
let n = script[p] as usize;
match n {
255 => return Expect::Stopped,
254 => return Expect::GuestRefused(files),
_ => {}
}
p += 1;
// A name or size reaching past the input reads guest memory beyond
// it: still bounded by the host, but not modelled here.
if p + n + 2 > script.len() {
return Expect::Unknown;
}
let size = u16::from_le_bytes([script[p + n], script[p + n + 1]]) as usize;
let Ok(name) = std::str::from_utf8(&script[p..p + n]) else {
return Expect::Stopped;
};
if check_file_name(name).is_err()
|| !seen.insert(name.to_string())
|| files.len() >= l.max_files
|| size > l.max_file_bytes
|| total + size > l.max_emit_bytes
{
return Expect::Stopped;
}
total += size;
files.push((name.to_string(), size));
p += n + 2;
}
Expect::Accepted(files)
}
fuzz_target!(|data: &[u8]| {
let l = limits();
let out = guest().run(data, &Default::default(), &l);
assert_ne!(
out.verdict.code(),
Some(codes::HOST_FAULT),
"{}",
out.verdict
);
// Whatever came out obeys every rule, whatever the script was.
let mut names = BTreeSet::new();
let mut total = 0usize;
assert!(out.files.len() <= l.max_files);
for f in &out.files {
assert_eq!(check_file_name(&f.name), Ok(()), "{:?}", f.name);
assert!(
heimdall_core::validate_output_name(&f.name).is_ok(),
"{:?}",
f.name
);
assert!(names.insert(f.name.clone()), "{:?} twice", f.name);
assert!(f.bytes.len() <= l.max_file_bytes);
total += f.bytes.len();
assert_eq!(f.entry(), Output::of(f.name.clone(), &f.bytes));
}
assert!(total <= l.max_emit_bytes);
if let Verdict::Refused { code, .. } = &out.verdict {
if *code != codes::GUEST_REFUSED {
assert!(out.files.is_empty(), "a trap or limit handed on files");
}
}
let got: Vec<(String, usize)> = out
.files
.iter()
.map(|f| (f.name.clone(), f.bytes.len()))
.collect();
match model(data, &l) {
Expect::Accepted(want) => {
assert!(out.verdict.is_accepted(), "{}", out.verdict);
assert_eq!(got, want);
}
Expect::GuestRefused(want) => {
assert_eq!(
out.verdict.code(),
Some(codes::GUEST_REFUSED),
"{}",
out.verdict
);
assert_eq!(got, want);
}
Expect::Stopped => {
assert!(
!out.verdict.is_accepted(),
"accepted a script that must stop"
);
assert_ne!(
out.verdict.code(),
Some(codes::GUEST_REFUSED),
"{}",
out.verdict
);
assert!(got.is_empty());
}
Expect::Unknown => {}
}
});