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

nordisk/edda

crates/guest-host/tests/no_alloc_before_bounds.rs

//! ★ **A guest's length is checked against its memory BEFORE the host allocates
//! for it** (code-smell review 2026-10-10, stinky #16): `read_mem` and the answer's
//! `read_bytes` made `vec![0u8; len]` from the guest's `len` and only then asked the
//! memory, so any guest could make the host allocate up to 4 GiB per call. A counting
//! allocator records the largest single request the host makes.

#![cfg(feature = "wasmtime")]

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

use guest_host::module::{read_arg, write_to_wasm, GuestState, LoadedModule};
use guest_host::{exports, BoundedLimiter, ModuleTrust, PluginLimits};
use wasmtime::Linker;

struct Counting;

static LARGEST: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, l: Layout) -> *mut u8 {
        LARGEST.fetch_max(l.size(), Ordering::Relaxed);
        unsafe { System.alloc(l) }
    }
    unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 {
        LARGEST.fetch_max(l.size(), Ordering::Relaxed);
        unsafe { System.alloc_zeroed(l) }
    }
    unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
        unsafe { System.dealloc(p, l) }
    }
    unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
        LARGEST.fetch_max(n, Ordering::Relaxed);
        unsafe { System.realloc(p, l, n) }
    }
}

#[global_allocator]
static A: Counting = Counting;

struct State {
    limits: BoundedLimiter,
    faults: Vec<String>,
}

impl GuestState for State {
    fn limiter(&mut self) -> &mut BoundedLimiter {
        &mut self.limits
    }
    fn faults(&mut self) -> &mut Vec<String> {
        &mut self.faults
    }
}

/// Op 8 hands the host import a ~4 GiB argument; op 9 answers a ~4 GiB result.
const LIAR: &str = r#"(module
  (import "env" "host_echo" (func $echo (param i32 i32) (result i64)))
  (memory (export "memory") 1)
  (global $bump (mut i32) (i32.const 1024))
  (func (export "alloc") (param $n i32) (result i32)
    (local $p i32)
    (local.set $p (global.get $bump))
    (global.set $bump (i32.add (global.get $bump) (local.get $n)))
    (local.get $p))
  (func (export "dealloc") (param i32 i32))
  (func (export "result_len") (result i32) (i32.const 4))
  (func (export "plugin_abi") (result i64) (i64.const 0x100000001))
  (func (export "plugin_manifest") (result i32) (i32.const 16))
  (func (export "plugin_call") (param $op i32) (param $ptr i32) (param $len i32) (result i64)
    (if (i32.eq (local.get $op) (i32.const 8))
      (then (return (call $echo (i32.const 16) (i32.const 0xFFFFFF00)))))
    (i64.or (i64.shl (i64.const 16) (i64.const 32)) (i64.const 0xFFFFFF00)))
)"#;

const LIMIT: usize = 64 << 20;

#[test]
fn a_length_beyond_the_guests_memory_is_refused_before_anything_is_allocated() {
    let dir = std::env::temp_dir().join(format!("guest-host-liar-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join("liar.wasm");
    std::fs::write(&path, wat::parse_str(LIAR).unwrap()).unwrap();
    let m = LoadedModule::load(&path, &PluginLimits::default(), &ModuleTrust::NotRequired, exports::ALL).unwrap();
    let mut l: Linker<State> = Linker::new(m.engine());
    l.func_wrap("env", "host_echo", |mut caller: wasmtime::Caller<'_, State>, ptr: u32, len: u32| -> u64 {
        let Some(arg) = read_arg(&mut caller, ptr, len, "host_echo") else { return 0 };
        write_to_wasm(&mut caller, &arg)
    })
    .unwrap();
    let state = || State { limits: m.limits().limiter(), faults: Vec::new() };

    LARGEST.store(0, Ordering::Relaxed);
    let mut inst = m.instantiate(state(), &l).unwrap();
    let _ = m.call_op(&mut inst, 8, b"x");
    let after_arg = LARGEST.load(Ordering::Relaxed);
    assert!(after_arg < LIMIT, "a host import's argument: the host asked for {after_arg} bytes");

    LARGEST.store(0, Ordering::Relaxed);
    let mut inst = m.instantiate(state(), &l).unwrap();
    let err = m.call_op(&mut inst, 9, b"x").unwrap_err();
    let after_answer = LARGEST.load(Ordering::Relaxed);
    assert!(after_answer < LIMIT, "a guest's answer: the host asked for {after_answer} bytes");
    assert!(err.to_string().contains("outside linear memory"), "{err}");
    let _ = std::fs::remove_dir_all(&dir);
}