Gunnar

nordisk/heimdall

crates/heimdall-wasm/tests/refusal_codes.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! Every kind of refusal carries its stable `heimdall-wasm-<n>` code, so a
//! caller branches on the code and never parses the reason text.
//! Heimdall is a defensive sandbox.

mod common;
use common::*;
use heimdall_wasm::{codes, RunLimits};
use std::time::Duration;

fn code_of(kit: &Kit, name: &str, wat: Wat, input: &[u8], limits: &RunLimits) -> &'static str {
    let g = kit.guest(name, &wat.text());
    g.run(input, &none(), limits)
        .verdict
        .code()
        .unwrap_or_else(|| panic!("{name} was accepted"))
}

#[test]
fn each_run_refusal_has_its_own_code() {
    let kit = Kit::new("codes");
    let spin = || Wat::new("(loop $l (br $l)) unreachable");
    let l = RunLimits::default();
    let cases: Vec<(&str, Wat, &[u8], RunLimits, &str)> = vec![
        (
            "refuse",
            Wat::new("(call $refuse (i32.const 16) (i32.const 1))")
                .extra(r#"(data (i32.const 16) "n")"#),
            b"",
            l.clone(),
            codes::GUEST_REFUSED,
        ),
        (
            "abort",
            Wat::new("unreachable"),
            b"",
            l.clone(),
            codes::TRAPPED,
        ),
        (
            "fuel",
            spin(),
            b"",
            RunLimits {
                fuel: 100_000,
                ..l.clone()
            },
            codes::OUT_OF_FUEL,
        ),
        (
            "clock",
            spin(),
            b"",
            RunLimits {
                fuel: u64::MAX,
                timeout: Duration::from_millis(100),
                ..l.clone()
            },
            codes::TIMEOUT,
        ),
        (
            "mem",
            Wat::new("(drop (memory.grow (i32.const 1000))) unreachable"),
            b"",
            tight(),
            codes::MEMORY_LIMIT,
        ),
        (
            "table",
            Wat::new("(drop (table.grow $t (ref.null func) (i32.const 1000000))) unreachable")
                .extra("(table $t 1 funcref)"),
            b"",
            tight(),
            codes::TABLE_LIMIT,
        ),
        (
            "stack",
            Wat::new("(call $d)").extra("(func $d (result i32) (call $d))"),
            b"",
            RunLimits {
                fuel: u64::MAX,
                ..l.clone()
            },
            codes::STACK_OVERFLOW,
        ),
        (
            "inst",
            Wat::new("(call $ok (i32.const 0) (i32.const 0))"),
            b"",
            RunLimits {
                instances: 1,
                ..l.clone()
            },
            codes::INSTANTIATION_LIMIT,
        ),
        (
            "emit",
            Wat::new(
                "(call $emit (i32.const 16) (i32.const 2) (i32.const 0) (i32.const 0)) unreachable",
            )
            .extra(r#"(data (i32.const 16) "..")"#),
            b"",
            l.clone(),
            codes::EMIT_REFUSED,
        ),
        (
            "note",
            Wat::new("(call $ok (i32.const 2048) (i32.const 1500))"),
            b"",
            RunLimits {
                max_text_bytes: 1000,
                ..l.clone()
            },
            codes::OUTPUT_TOO_LARGE,
        ),
    ];
    for (name, wat, input, limits, want) in cases {
        assert_eq!(code_of(&kit, name, wat, input, &limits), want, "{name}");
    }
}

#[test]
fn a_wrong_run_signature_has_the_interface_code() {
    let kit = Kit::new("code-iface");
    let wit = "package heimdall:attack@0.1.0;\nworld other { export run: func() -> u32; }";
    let core =
        r#"(module (memory (export "memory") 1) (func (export "run") (result i32) (i32.const 7)))"#;
    let g = kit
        .try_load("runtype", &component_with(core, &[("o.wit", wit)], "other"))
        .unwrap();
    assert_eq!(
        g.run(b"", &none(), &RunLimits::default()).verdict.code(),
        Some(codes::INTERFACE_MISMATCH)
    );
}

#[test]
fn each_load_refusal_has_its_own_code() {
    let kit = Kit::new("load-codes");
    let host = kit.host();
    assert_eq!(
        host.load(&kit.dir.join("none.wasm")).err().unwrap().code(),
        codes::LOAD_UNREADABLE
    );
    let p = kit.place("good", &component(&good_wat()));
    assert_eq!(
        host.load_pinned(&p, &"0".repeat(64)).err().unwrap().code(),
        codes::LOAD_DIGEST_MISMATCH
    );
    std::fs::remove_file(sidecar(&p, "p7s")).unwrap();
    assert_eq!(host.load(&p).err().unwrap().code(), codes::LOAD_UNTRUSTED);
    std::fs::remove_file(sidecar(&p, "blake3")).unwrap();
    assert_eq!(
        host.load(&p).err().unwrap().code(),
        codes::LOAD_DIGEST_MISSING
    );
    let core = wat::parse_str("(module)").unwrap();
    assert_eq!(
        kit.try_load("core", &core).err().unwrap().code(),
        codes::LOAD_NOT_A_COMPONENT
    );
    assert_eq!(
        kit.try_load("junk", b"junk").err().unwrap().code(),
        codes::LOAD_INVALID
    );
    let e = kit.try_load("junk2", b"junk").err().unwrap();
    assert!(e.to_string().ends_with("[heimdall-wasm-26]"), "{e}");
}