nordisk/heimdall
fuzz/common/wasm_kit.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Heimdall is a defensive sandbox. Shared kit for the heimdall-wasm fuzz
//! targets: a throwaway CA and release signer (holger's plugin signature
//! format through guest-host), a host trusting only that CA, a per-process
//! scratch directory, and the emit-script guest. Nothing here is a secret.
#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use guest_host::verify::sign::{dev, sign_module, Ed25519Signer};
use heimdall_wasm::{Host, HostConfig, WIT, WORLD};
pub struct Kit {
pub dir: PathBuf,
pub signer: Ed25519Signer,
pub host: Host,
}
pub fn kit() -> &'static Kit {
static KIT: OnceLock<Kit> = OnceLock::new();
KIT.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("heimdall-fuzz-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
let (ca_key, ca_der) = dev::mint_ca("heimdall-fuzz-ca").expect("ca");
let root = dir.join("root.der");
std::fs::write(&root, &ca_der).expect("root");
let signer =
dev::new_ed25519_signer(&ca_key, &ca_der, "heimdall-fuzz-release").expect("signer");
let host = Host::new(HostConfig::new([root])).expect("host");
Kit { dir, signer, host }
})
}
pub fn sidecar(module: &Path, ext: &str) -> PathBuf {
let mut n = module.file_name().expect("file name").to_os_string();
n.push(".");
n.push(ext);
module.with_file_name(n)
}
pub fn blake3_hex(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
impl Kit {
/// Write `bytes` as `<name>.wasm` with a valid `.p7s` and `.blake3`.
pub fn place_signed(&self, name: &str, bytes: &[u8]) -> PathBuf {
let module = self.dir.join(format!("{name}.wasm"));
std::fs::write(&module, bytes).expect("module");
let cms = sign_module(bytes, &self.signer).expect("sign");
std::fs::write(sidecar(&module, "p7s"), cms).expect("p7s");
std::fs::write(
sidecar(&module, "blake3"),
format!("{}\n", blake3_hex(bytes)),
)
.expect("pin");
module
}
}
/// Componentize a core WAT module against heimdall's own WIT world.
pub fn component(core_wat: &str) -> Vec<u8> {
let mut resolve = wit_parser::Resolve::default();
let pkg = resolve.push_str("heimdall.wit", WIT).expect("wit");
let world = resolve.select_world(&[pkg], Some(WORLD)).expect("world");
let mut core = wat::parse_str(core_wat).expect("wat");
wit_component::embed_component_metadata(
&mut core,
&resolve,
world,
wit_component::StringEncoding::UTF8,
)
.expect("embed");
wit_component::ComponentEncoder::default()
.module(&core)
.expect("module")
.validate(true)
.encode()
.expect("encode")
}
/// The emit-script guest. Its input is a script of steps:
/// 0xFF trap (unreachable)
/// 0xFE return err(refusal) "stop"
/// n (0..=253) emit-file(name = next n bytes, bytes = memory[0..u16 LE
/// that follows the name]), then continue
/// At the end of the input it returns ok("done"). Memory holds "done" at 16,
/// the result record at 1024 and the input from 4096 on.
pub const EMIT_SCRIPT_WAT: &str = r#"(module
(import "$root" "emit-file" (func $emit (param i32 i32 i32 i32)))
(memory (export "memory") 2)
(global $heap (mut i32) (i32.const 4096))
(data (i32.const 16) "donestop")
(func $realloc (export "cabi_realloc") (param $old i32) (param $old_size i32) (param $align i32) (param $size i32) (result i32)
(local $p i32)
(local.set $p (i32.and (i32.add (global.get $heap) (i32.sub (local.get $align) (i32.const 1)))
(i32.xor (i32.sub (local.get $align) (i32.const 1)) (i32.const -1))))
(global.set $heap (i32.add (local.get $p) (local.get $size)))
(block $done
(loop $grow
(br_if $done (i64.le_u (i64.extend_i32_u (global.get $heap)) (i64.mul (i64.extend_i32_u (memory.size)) (i64.const 65536))))
(if (i32.eq (memory.grow (i32.const 1)) (i32.const -1)) (then unreachable))
(br $grow)))
(local.get $p))
(func $result (param $tag i32) (param $p i32) (param $l i32) (result i32)
(i32.store8 (i32.const 1024) (local.get $tag))
(i32.store (i32.const 1028) (local.get $p))
(i32.store (i32.const 1032) (local.get $l))
(i32.const 1024))
(func (export "run") (param $in i32) (param $in_len i32) (param $params i32) (param $params_len i32) (result i32)
(local $p i32) (local $end i32) (local $nl i32) (local $sz i32)
(local.set $p (local.get $in))
(local.set $end (i32.add (local.get $in) (local.get $in_len)))
(block $done
(loop $l
(br_if $done (i32.ge_u (local.get $p) (local.get $end)))
(local.set $nl (i32.load8_u (local.get $p)))
(if (i32.eq (local.get $nl) (i32.const 255)) (then unreachable))
(if (i32.eq (local.get $nl) (i32.const 254))
(then (return (call $result (i32.const 1) (i32.const 20) (i32.const 4)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(local.set $sz (i32.load16_u (i32.add (local.get $p) (local.get $nl))))
(call $emit (local.get $p) (local.get $nl) (i32.const 0) (local.get $sz))
(local.set $p (i32.add (local.get $p) (i32.add (local.get $nl) (i32.const 2))))
(br $l)))
(call $result (i32.const 0) (i32.const 16) (i32.const 4)))
)"#;