Gunnar

nordisk/heimdall

fuzz/fuzz_targets/wasm_guest_load.rs

// 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}"
                ),
                _ => {}
            }
        }
    }
});