// SPDX-License-Identifier: AGPL-3.0-only
//! Heimdall is a defensive sandbox. Fuzz the wasm host's load path with
//! arbitrary bytes as a guest module: the blake3 pin, the signature check and
//! the compile/import/export checks must refuse without a panic, only a
//! correctly pinned and signed component may load, and a loaded guest's run
//! never ends in a host fault.
//!
//! Input: byte 0 picks the case, the rest is the module.
//! 0, 4.. signed and pinned correctly (reaches the compiler)
//! 1 no `.wasm.p7s` (unsigned)
//! 2 `.wasm.p7s` = fuzzer bytes (first byte = their length)
//! 3 signed, but the `.wasm.blake3` pin is of other bytes
#![no_main]
use libfuzzer_sys::fuzz_target;
#[path = "../common/wasm_kit.rs"]
mod wasm_kit;
use heimdall_wasm::{codes, RunLimits};
use std::time::Duration;
use wasm_kit::{blake3_hex, kit, sidecar};
fuzz_target!(|data: &[u8]| {
let Some((&case, rest)) = data.split_first() else {
return;
};
let kit = kit();
let module = kit.dir.join("guest.wasm");
let p7s = sidecar(&module, "p7s");
let pin = sidecar(&module, "blake3");
let case = if case < 4 { case } else { 0 };
let bytes: &[u8] = match case {
2 => {
let n = rest
.first()
.map_or(0, |&n| n as usize)
.min(rest.len().saturating_sub(1));
let (sig, m) = rest.get(1..).unwrap_or(&[]).split_at(n);
std::fs::write(&module, m).unwrap();
std::fs::write(&p7s, sig).unwrap();
std::fs::write(&pin, format!("{}\n", blake3_hex(m))).unwrap();
m
}
_ => {
kit.place_signed("guest", rest);
match case {
1 => std::fs::remove_file(&p7s).unwrap(),
3 => {
let mut other = rest.to_vec();
other.push(0);
std::fs::write(&pin, format!("{}\n", blake3_hex(&other))).unwrap();
}
_ => {}
}
rest
}
};
match kit.host.load(&module) {
Ok(guest) => {
assert_eq!(case, 0, "a guest loaded without a valid pin and signature");
assert_eq!(guest.digest_hex(), blake3_hex(bytes));
assert!(
!bytes.starts_with(b"\0asm\x01\0\0\0"),
"a core module loaded"
);
let limits = RunLimits {
fuel: 5_000_000,
timeout: Duration::from_millis(500),
memory_bytes: 16 << 20,
..RunLimits::default()
};
let out = guest.run(b"fuzz", &Default::default(), &limits);
assert_ne!(
out.verdict.code(),
Some(codes::HOST_FAULT),
"{}",
out.verdict
);
}
Err(refusal) => {
assert!(codes::ALL.contains(&refusal.code()));
let text = refusal.to_string();
assert!(text.starts_with("refused: guest not loaded ("), "{text}");
match case {
1 | 3 => assert!(
matches!(
refusal.code(),
codes::LOAD_UNTRUSTED | codes::LOAD_DIGEST_MISMATCH
),
"case {case}: {text}"
),
_ => {}
}
}
}
});