guests/verify-bundle/fuzz/fuzz_targets/decoder_instance.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Heimdall is a defensive sandbox. Fuzz ONE OpenZL decoder instance through
//! the real host: the split guest's decoder component (OpenZL's C built for
//! wasm32, the 32-bit build the native `openzl_frame` target never runs),
//! signed, loaded by `Host::load_decoder` and driven by `Decoder::decode`,
//! exactly as a verifier's `decode-chunks` call is served.
//!
//! The declared size is what the frame's header says (as the archive reader
//! demands), capped at 16 MiB. Allowed outcomes: an answer (the host has then
//! checked its length and BLAKE3), a per-chunk refusal, or the instance's own
//! memory/fuel/clock limit. Anything else — a trap of the C code, a stack
//! overflow, a host fault, a decoder that answers falsely — is a finding.
//!
//! libFuzzer sees coverage of the host side only (wasm runs as JIT code), so
//! this target leans on its seeds: the native openzl_frame corpus.
//!
//! cargo build --release --lib --target wasm32-unknown-unknown --features decoder
//! HEIMDALL_DECODER_WASM=$PWD/target/wasm32-unknown-unknown/release/heimdall_guest_verify_bundle.wasm \
//! cargo +nightly fuzz run -s none decoder_instance
#![no_main]
use std::sync::OnceLock;
use heimdall_wasm::{codes, Decoder, Host, HostConfig, RunLimits};
use libfuzzer_sys::fuzz_target;
struct Lab {
_dir: std::path::PathBuf,
_host: Host,
decoder: Decoder,
}
fn lab() -> &'static Lab {
static L: OnceLock<Lab> = OnceLock::new();
L.get_or_init(|| {
use guest_host::verify::sign::{dev, sign_module};
let core = std::fs::read(
std::env::var("HEIMDALL_DECODER_WASM").expect("set HEIMDALL_DECODER_WASM"),
)
.unwrap();
let comp = wit_component::ComponentEncoder::default()
.module(&core)
.unwrap()
.validate(true)
.encode()
.unwrap();
let dir = std::env::temp_dir().join(format!("heimdall-fuzz-decoder-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (ca_key, ca_der) = dev::mint_ca("heimdall-fuzz-ca").unwrap();
let root = dir.join("root.der");
std::fs::write(&root, &ca_der).unwrap();
let signer = dev::new_ed25519_signer(&ca_key, &ca_der, "heimdall-fuzz").unwrap();
let m = dir.join("decoder.wasm");
std::fs::write(&m, &comp).unwrap();
std::fs::write(dir.join("decoder.wasm.p7s"), sign_module(&comp, &signer).unwrap()).unwrap();
std::fs::write(
dir.join("decoder.wasm.blake3"),
blake3::hash(&comp).to_hex().to_string(),
)
.unwrap();
let host = Host::new(HostConfig::new([root])).unwrap();
let decoder = host.load_decoder(&m).expect("the decoder loads");
Lab {
_dir: dir,
_host: host,
decoder,
}
})
}
fuzz_target!(|data: &[u8]| {
let Ok(size) = heimdall_guest_verify_bundle::zl::frame_size(data) else {
return;
};
if size > 16 << 20 {
return;
}
let limits = RunLimits {
decoders: 1,
fuel: 2_000_000_000,
timeout: std::time::Duration::from_secs(20),
..RunLimits::default()
};
let run = lab().decoder.decode(&[(data, size)], &limits);
match run.result {
Ok(answers) => {
assert_eq!(answers.len(), 1);
if let Ok(d) = &answers[0] {
assert_eq!(d.bytes.len() as u64, size);
assert_eq!(d.digest, blake3::hash(&d.bytes).as_bytes().to_vec());
}
}
Err(f) => assert!(
[codes::MEMORY_LIMIT, codes::OUT_OF_FUEL, codes::TIMEOUT].contains(&f.code),
"decoder instance finding: {} {}",
f.code,
f.reason
),
}
});