Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/heimdall

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
        ),
    }
});