Gunnar

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)))
)"#;