Rust wasmi integration: WASM sandbox with fuel metering and memory export
ober
40960e650d6efcfd4af65d532f0bd13b3df1d93e
--- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -30,6 +30,24 @@ dependencies = [ ] [[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -59,6 +77,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -203,6 +230,12 @@ dependencies = [ ] [[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -227,6 +260,12 @@ dependencies = [ ] [[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -267,6 +306,12 @@ dependencies = [ ] [[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] name = "futures-channel" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -349,12 +394,27 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -376,6 +436,16 @@ dependencies = [ ] [[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] name = "inotify" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -408,6 +478,7 @@ dependencies = [ name = "jerboa-native" version = "0.1.0" dependencies = [ + "argon2", "ed25519-dalek", "flate2", "hkdf", @@ -424,6 +495,7 @@ dependencies = [ "scrypt", "sha2", "time", + "wasmi", "webpki-roots 0.26.11", "x25519-dalek", ] @@ -445,6 +517,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] name = "libredox" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -517,6 +595,12 @@ dependencies = [ ] [[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1023,6 +1107,12 @@ dependencies = [ ] [[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1033,6 +1123,16 @@ dependencies = [ ] [[package]] +name = "string-interner" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" +dependencies = [ + "hashbrown 0.15.5", + "serde", +] + +[[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1277,6 +1377,60 @@ dependencies = [ ] [[package]] +name = "wasmi" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" +dependencies = [ + "string-interner", +] + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags", + "indexmap", +] + +[[package]] name = "web-sys" version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -25,6 +25,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "logg rustls-pki-types = "1" rustls-pemfile = "2" webpki-roots = "0.26" +wasmi = "0.40" [target.'cfg(target_os = "linux")'.dependencies] inotify = { version = "0.11", default-features = false } --- a/jerboa-native-rs/src/lib.rs +++ b/jerboa-native-rs/src/lib.rs @@ -12,6 +12,7 @@ mod integrity; mod x25519; mod ed25519; mod process_ctl; +mod wasm; #[cfg(target_os = "linux")] mod epoll; new file mode 100644 --- /dev/null +++ b/jerboa-native-rs/src/wasm.rs @@ -0,0 +1,471 @@ +//! Rust-side WASM runtime via wasmi. +//! +//! Provides a handle-based FFI for loading and executing WASM modules +//! from Chez Scheme. This is the "critical sections in Rust VM" path: +//! security-sensitive parsers run inside wasmi's sandbox, isolated from +//! the Chez Scheme address space and its ROP gadget surface. +//! +//! Architecture: +//! Scheme code → FFI → wasmi interpreter → WASM bytecode +//! +//! The wasmi interpreter is memory-safe Rust. Even with arbitrary write +//! inside WASM linear memory, an attacker cannot reach Chez runtime gadgets. + +use std::collections::HashMap; +use std::sync::Mutex; + +use wasmi::*; +use wasmi::core::ValType; + +use crate::panic::{ffi_wrap, set_last_error}; + +// ============================================================ +// Handle management +// ============================================================ + +struct WasmModule { + engine: Engine, + module: Module, +} + +struct WasmInstance { + store: Store<()>, + instance: Instance, +} + +macro_rules! lazy_handles { + ($($name:ident: $type:ty),* $(,)?) => { + $( + fn $name() -> &'static Mutex<$type> { + use std::sync::OnceLock; + static INSTANCE: OnceLock<Mutex<$type>> = OnceLock::new(); + INSTANCE.get_or_init(|| Mutex::new(HashMap::new())) + } + )* + }; +} + +lazy_handles! { + wasm_modules: HashMap<u64, WasmModule>, + wasm_instances: HashMap<u64, WasmInstance>, +} + +static NEXT_WASM_HANDLE: std::sync::atomic::AtomicU64 = + std::sync::atomic::AtomicU64::new(1); + +fn next_handle() -> u64 { + NEXT_WASM_HANDLE.fetch_add(1, std::sync::atomic::Ordering::Relaxed) +} + +// ============================================================ +// Module: load and validate WASM bytecode +// ============================================================ + +/// Load a WASM module from bytecode. +/// Returns a module handle (>0) on success, 0 on error. +/// Engine is configured with fuel metering for deterministic termination. +#[no_mangle] +pub extern "C" fn jerboa_wasm_module_new( + bytes: *const u8, + bytes_len: usize, +) -> u64 { + match std::panic::catch_unwind(|| { + if bytes.is_null() || bytes_len == 0 { + set_last_error("null or empty WASM bytecode".to_string()); + return 0; + } + + let wasm_bytes = unsafe { std::slice::from_raw_parts(bytes, bytes_len) }; + + let mut config = Config::default(); + config.consume_fuel(true); + let engine = Engine::new(&config); + + let module = match Module::new(&engine, wasm_bytes) { + Ok(m) => m, + Err(e) => { + set_last_error(format!("WASM module validation failed: {e}")); + return 0; + } + }; + + let handle = next_handle(); + wasm_modules() + .lock() + .unwrap() + .insert(handle, WasmModule { engine, module }); + handle + }) { + Ok(h) => h, + Err(_) => { + set_last_error("panic in jerboa_wasm_module_new".to_string()); + 0 + } + } +} + +/// Free a WASM module. +#[no_mangle] +pub extern "C" fn jerboa_wasm_module_free(handle: u64) { + let _ = wasm_modules().lock().unwrap().remove(&handle); +} + +// ============================================================ +// Instance: instantiate a module for execution +// ============================================================ + +/// Instantiate a WASM module (no imports — pure computation). +/// `fuel` = max instructions (0 = default 10M). +/// Returns instance handle (>0) on success, 0 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_instance_new( + module_handle: u64, + fuel: u64, +) -> u64 { + match std::panic::catch_unwind(|| { + let modules = wasm_modules().lock().unwrap(); + let wmod = match modules.get(&module_handle) { + Some(m) => m, + None => { + set_last_error("invalid module handle".to_string()); + return 0; + } + }; + + let mut store = Store::new(&wmod.engine, ()); + let fuel_amount = if fuel == 0 { 10_000_000 } else { fuel }; + let _ = store.set_fuel(fuel_amount); + + let linker = Linker::new(&wmod.engine); + + let pre = match linker.instantiate(&mut store, &wmod.module) { + Ok(pre) => pre, + Err(e) => { + set_last_error(format!("WASM instantiation failed: {e}")); + return 0; + } + }; + + let instance = match pre.start(&mut store) { + Ok(inst) => inst, + Err(e) => { + set_last_error(format!("WASM start function failed: {e}")); + return 0; + } + }; + + let handle = next_handle(); + wasm_instances() + .lock() + .unwrap() + .insert(handle, WasmInstance { store, instance }); + handle + }) { + Ok(h) => h, + Err(_) => { + set_last_error("panic in jerboa_wasm_instance_new".to_string()); + 0 + } + } +} + +/// Free a WASM instance. +#[no_mangle] +pub extern "C" fn jerboa_wasm_instance_free(handle: u64) { + let _ = wasm_instances().lock().unwrap().remove(&handle); +} + +/// Add fuel to an existing instance. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_add_fuel(handle: u64, fuel: u64) -> i32 { + ffi_wrap(|| { + let mut instances = wasm_instances().lock().unwrap(); + let inst = match instances.get_mut(&handle) { + Some(i) => i, + None => { + set_last_error("invalid instance handle".to_string()); + return -1; + } + }; + match inst.store.set_fuel(fuel) { + Ok(()) => 0, + Err(e) => { + set_last_error(format!("set_fuel failed: {e}")); + -1 + } + } + }) +} + +/// Get remaining fuel for an instance. +/// Returns fuel remaining, or -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_fuel_remaining(handle: u64) -> i64 { + match std::panic::catch_unwind(|| { + let instances = wasm_instances().lock().unwrap(); + let inst = match instances.get(&handle) { + Some(i) => i, + None => return -1i64, + }; + inst.store.get_fuel().unwrap_or(0) as i64 + }) { + Ok(f) => f, + Err(_) => -1, + } +} + +// ============================================================ +// Call: invoke an exported function +// ============================================================ + +/// Call an exported WASM function by name. +/// +/// Arguments and results are passed as i64 arrays. For i32 params, +/// the value is truncated; for f32/f64, it's reinterpreted from bits. +/// +/// Returns the number of results on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_call( + handle: u64, + name: *const u8, + name_len: usize, + args: *const i64, + nargs: usize, + results: *mut i64, + nresults: usize, +) -> i32 { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + if name.is_null() { + set_last_error("null function name".to_string()); + return -1; + } + + let func_name = match std::str::from_utf8(unsafe { + std::slice::from_raw_parts(name, name_len) + }) { + Ok(s) => s, + Err(_) => { + set_last_error("invalid UTF-8 function name".to_string()); + return -1; + } + }; + + let mut instances = wasm_instances().lock().unwrap(); + let inst = match instances.get_mut(&handle) { + Some(i) => i, + None => { + set_last_error("invalid instance handle".to_string()); + return -1; + } + }; + + let func = match inst.instance.get_func(&inst.store, func_name) { + Some(f) => f, + None => { + set_last_error(format!("export not found: {func_name}")); + return -1; + } + }; + + // Build args matching WASM function signature + let func_type = func.ty(&inst.store); + let param_types: Vec<ValType> = func_type.params().iter().cloned().collect(); + + if param_types.len() != nargs { + set_last_error(format!( + "argument count mismatch: expected {} got {}", + param_types.len(), + nargs + )); + return -1; + } + + let wasm_args: Vec<Val> = if nargs > 0 && !args.is_null() { + let arg_slice = unsafe { std::slice::from_raw_parts(args, nargs) }; + arg_slice + .iter() + .zip(param_types.iter()) + .map(|(&val, ty)| match ty { + ValType::I32 => Val::I32(val as i32), + ValType::I64 => Val::I64(val), + ValType::F32 => Val::F32(f32::from_bits(val as u32).into()), + ValType::F64 => Val::F64(f64::from_bits(val as u64).into()), + _ => Val::I32(val as i32), + }) + .collect() + } else { + vec![] + }; + + // Prepare result slots + let result_types: Vec<ValType> = func_type.results().iter().cloned().collect(); + let actual_nresults = result_types.len(); + let mut wasm_results: Vec<Val> = result_types + .iter() + .map(|ty| match ty { + ValType::I32 => Val::I32(0), + ValType::I64 => Val::I64(0), + ValType::F32 => Val::F32(0.0f32.into()), + ValType::F64 => Val::F64(0.0f64.into()), + _ => Val::I32(0), + }) + .collect(); + + // Execute + if let Err(e) = func.call(&mut inst.store, &wasm_args, &mut wasm_results) { + set_last_error(format!("WASM trap: {e}")); + return -1; + } + + // Copy results out + if !results.is_null() && nresults > 0 { + let out = unsafe { std::slice::from_raw_parts_mut(results, nresults) }; + for (i, val) in wasm_results.into_iter().enumerate() { + if i >= nresults { + break; + } + out[i] = match val { + Val::I32(v) => v as i64, + Val::I64(v) => v, + Val::F32(v) => f32::to_bits(v.into()) as i64, + Val::F64(v) => f64::to_bits(v.into()) as i64, + _ => 0, + }; + } + } + + actual_nresults as i32 + })) { + Ok(r) => r, + Err(_) => { + set_last_error("panic in jerboa_wasm_call".to_string()); + -1 + } + } +} + +// ============================================================ +// Memory: read/write WASM linear memory from host +// ============================================================ + +/// Read bytes from WASM linear memory. +/// Returns number of bytes read on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_memory_read( + handle: u64, + offset: u32, + buf: *mut u8, + len: u32, +) -> i32 { + ffi_wrap(|| { + if buf.is_null() { + set_last_error("null buffer".to_string()); + return -1; + } + + let instances = wasm_instances().lock().unwrap(); + let inst = match instances.get(&handle) { + Some(i) => i, + None => { + set_last_error("invalid instance handle".to_string()); + return -1; + } + }; + + let memory = match inst.instance.get_memory(&inst.store, "memory") { + Some(m) => m, + None => { + set_last_error("no 'memory' export".to_string()); + return -1; + } + }; + + let mem_data = memory.data(&inst.store); + let start = offset as usize; + let end = start + len as usize; + + if end > mem_data.len() { + set_last_error(format!( + "memory read OOB: offset={offset} len={len} size={}", + mem_data.len() + )); + return -1; + } + + let out = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) }; + out.copy_from_slice(&mem_data[start..end]); + len as i32 + }) +} + +/// Write bytes to WASM linear memory. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_memory_write( + handle: u64, + offset: u32, + buf: *const u8, + len: u32, +) -> i32 { + ffi_wrap(|| { + if buf.is_null() { + set_last_error("null buffer".to_string()); + return -1; + } + + let mut instances = wasm_instances().lock().unwrap(); + let inst = match instances.get_mut(&handle) { + Some(i) => i, + None => { + set_last_error("invalid instance handle".to_string()); + return -1; + } + }; + + let memory = match inst.instance.get_memory(&inst.store, "memory") { + Some(m) => m, + None => { + set_last_error("no 'memory' export".to_string()); + return -1; + } + }; + + let mem_data = memory.data_mut(&mut inst.store); + let start = offset as usize; + let end = start + len as usize; + + if end > mem_data.len() { + set_last_error(format!( + "memory write OOB: offset={offset} len={len} size={}", + mem_data.len() + )); + return -1; + } + + let input = unsafe { std::slice::from_raw_parts(buf, len as usize) }; + mem_data[start..end].copy_from_slice(input); + 0 + }) +} + +/// Get the size of WASM linear memory in bytes. +/// Returns size on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_wasm_memory_size(handle: u64) -> i64 { + match std::panic::catch_unwind(|| { + let instances = wasm_instances().lock().unwrap(); + let inst = match instances.get(&handle) { + Some(i) => i, + None => return -1i64, + }; + + match inst.instance.get_memory(&inst.store, "memory") { + Some(m) => m.data(&inst.store).len() as i64, + None => -1, + } + }) { + Ok(s) => s, + Err(_) => -1, + } +} --- a/lib/jerboa/wasm/codegen.sls +++ b/lib/jerboa/wasm/codegen.sls @@ -1096,6 +1096,10 @@ (wasm-export-func (symbol->string (car pair)) idx))))) funcs)) + ;; Auto-export memory as "memory" (WASM convention for host access) + (unless (null? (wasm-module-memories mod)) + (wasm-module-add-export! mod (wasm-export-memory "memory" 0))) + ;; Process data segments (for-each (lambda (form) new file mode 100644 --- /dev/null +++ b/tests/test-wasm-sandbox.ss @@ -0,0 +1,251 @@ +#!chezscheme +;;; test-wasm-sandbox.ss — Tests for Rust wasmi WASM sandbox +;;; +;;; Verifies that WASM modules compiled from Scheme execute correctly +;;; inside the Rust wasmi interpreter, fully isolated from Chez Scheme. + +(import (except (chezscheme) compile-program) + (jerboa wasm format) + (jerboa wasm codegen) + (std wasm sandbox)) + +(define pass 0) +(define fail 0) + +(define-syntax test + (syntax-rules () + [(_ name expr expected) + (guard (exn [#t (set! fail (+ fail 1)) + (printf "FAIL ~a: ~a~%" name + (if (message-condition? exn) (condition-message exn) exn))]) + (let ([got expr]) + (if (equal? got expected) + (begin (set! pass (+ pass 1)) (printf " ok ~a~%" name)) + (begin (set! fail (+ fail 1)) + (printf "FAIL ~a: got ~s expected ~s~%" name got expected)))))])) + +(define (string-contains s sub) + (let ([slen (string-length s)] [sublen (string-length sub)]) + (let lp ([i 0]) + (cond + [(> (+ i sublen) slen) #f] + [(string=? (substring s i (+ i sublen)) sub) i] + [else (lp (+ i 1))])))) + +(printf "--- Rust wasmi Sandbox Tests ---~%~%") + +;;; ============================================================ +;;; Section 1: Availability +;;; ============================================================ +(printf "--- Section 1: Availability ---~%") + +(test "wasmi sandbox is available" + (wasm-sandbox-available?) + #t) + +;;; ============================================================ +;;; Section 2: Basic computation +;;; ============================================================ +(printf "~%--- Section 2: Basic computation ---~%") + +(test "factorial(10) in wasmi" + (let* ([bv (compile-program + '((define (factorial n) + (if (= n 0) 1 (* n (factorial (- n 1)))))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (let ([r (wasm-sandbox-call inst "factorial" 10)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 3628800) + +(test "fibonacci(20) in wasmi" + (let* ([bv (compile-program + '((define (fib n) + (if (<= n 1) n + (+ (fib (- n 1)) (fib (- n 2)))))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (let ([r (wasm-sandbox-call inst "fib" 20)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 6765) + +(test "arithmetic in wasmi" + (let* ([bv (compile-program + '((define (compute (a i32) (b i32) -> i32) + (+ (* a a) (* b b)))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (let ([r (wasm-sandbox-call inst "compute" 3 4)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 25) + +;;; ============================================================ +;;; Section 3: Memory operations +;;; ============================================================ +(printf "~%--- Section 3: Memory operations ---~%") + +(test "memory store and load via WASM" + (let* ([bv (compile-program + '((define-memory 1) + (define (store-val (addr i32) (val i32) -> i32) + (i32.store addr val) val) + (define (load-val (addr i32) -> i32) + (i32.load addr))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (wasm-sandbox-call inst "store-val" 0 42) + (let ([r (wasm-sandbox-call inst "load-val" 0)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 42) + +(test "host memory read" + (let* ([bv (compile-program + '((define-memory 1) + (define (store-val (addr i32) (val i32) -> i32) + (i32.store addr val) val)))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (wasm-sandbox-call inst "store-val" 0 42) + (let ([data (wasm-sandbox-memory-read inst 0 4)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + data)) + #vu8(42 0 0 0)) + +(test "host memory write then WASM read" + (let* ([bv (compile-program + '((define-memory 1) + (define (load-val (addr i32) -> i32) + (i32.load addr))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (wasm-sandbox-memory-write inst 0 (bytevector 99 0 0 0)) + (let ([r (wasm-sandbox-call inst "load-val" 0)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 99) + +(test "memory size is 1 page (65536 bytes)" + (let* ([bv (compile-program + '((define-memory 1) + (define (nop) 0)))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h)]) + (let ([sz (wasm-sandbox-memory-size inst)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + sz)) + 65536) + +;;; ============================================================ +;;; Section 4: Fuel metering (deterministic termination) +;;; ============================================================ +(printf "~%--- Section 4: Fuel metering ---~%") + +(test "fuel exhaustion traps infinite loop" + (let* ([bv (compile-program '((define (spin) (while #t 0) 0)))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h 'fuel: 1000)]) + (guard (exn + [#t (let ([msg (if (message-condition? exn) (condition-message exn) "")]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + (and (string-contains msg "fuel") 'trapped))]) + (wasm-sandbox-call inst "spin") + 'no-trap)) + 'trapped) + +(test "sufficient fuel allows computation" + (let* ([bv (compile-program '((define (f x) (+ x 1))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h 'fuel: 10000000)]) + (let ([r (wasm-sandbox-call inst "f" 41)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + r)) + 42) + +(test "fuel remaining decreases after execution" + (let* ([bv (compile-program '((define (f x) (+ x 1))))] + [mod-h (wasm-sandbox-load bv)] + [inst (wasm-sandbox-instantiate mod-h 'fuel: 10000)]) + (let ([before (wasm-sandbox-fuel-remaining inst)]) + (wasm-sandbox-call inst "f" 1) + (let ([after (wasm-sandbox-fuel-remaining inst)]) + (wasm-sandbox-free inst) + (wasm-sandbox-free-module mod-h) + (< after before)))) + #t) + +;;; ============================================================ +;;; Section 5: Module validation +;;; ============================================================ +(printf "~%--- Section 5: Module validation ---~%") + +(test "invalid WASM bytecode rejected" + (guard (exn [#t 'rejected]) + (wasm-sandbox-load (bytevector 0 1 2 3 4 5 6 7)) + 'accepted) + 'rejected) + +(test "empty bytevector rejected" + (guard (exn [#t 'rejected]) + (wasm-sandbox-load (bytevector)) + 'accepted) + 'rejected) + +;;; ============================================================ +;;; Section 6: Multiple instances +;;; ============================================================ +(printf "~%--- Section 6: Multiple instances ---~%") + +(test "multiple instances from same module" + (let* ([bv (compile-program '((define (f x) (+ x 1))))] + [mod-h (wasm-sandbox-load bv)] + [inst1 (wasm-sandbox-instantiate mod-h)] + [inst2 (wasm-sandbox-instantiate mod-h)]) + (let ([r1 (wasm-sandbox-call inst1 "f" 10)] + [r2 (wasm-sandbox-call inst2 "f" 20)]) + (wasm-sandbox-free inst1) + (wasm-sandbox-free inst2) + (wasm-sandbox-free-module mod-h) + (+ r1 r2))) + 32) + +(test "instances have isolated memory" + (let* ([bv (compile-program + '((define-memory 1) + (define (store-val (addr i32) (val i32) -> i32) + (i32.store addr val) val) + (define (load-val (addr i32) -> i32) + (i32.load addr))))] + [mod-h (wasm-sandbox-load bv)] + [inst1 (wasm-sandbox-instantiate mod-h)] + [inst2 (wasm-sandbox-instantiate mod-h)]) + ;; Write different values in each instance