diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index b522cc8..d1ba56f 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -2401,3 +2401,206 @@ fn double_stop_is_idempotent() { let result = rt.stop_actor(addr); assert!(result.is_err(), "stopping already-stopped actor should error"); } + +// ── Disabled features: SIMD module rejected by sandboxed engine ───────────── + +#[test] +fn module_using_disabled_simd_is_rejected() { + // The engine disables SIMD. A module using v128 SIMD types should + // fail to compile or instantiate. + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32) + ;; v128.const is a SIMD instruction + v128.const i32x4 0 0 0 0 + drop + ) + ) + "#; + let result = wat::parse_str(wat); + // If wat parses it, try to compile with the sandboxed engine + match result { + Ok(wasm) => { + let engine = SharedEngine::new().unwrap(); + let build_result = WasmActorBuilder::new(engine, wasm).build(); + assert!(build_result.is_err(), "SIMD module should be rejected by sandboxed engine"); + } + Err(_) => { + // wat parser itself rejects SIMD — that's also fine + } + } +} + +// ── Garbage address in send: any 32 bytes accepted ────────────────────────── + +#[test] +fn send_with_garbage_address_bytes_silently_fails() { + // Guest sends to an address that's 32 random/garbage bytes. + // The runtime can't route to it — ctx.send() returns Err, which is + // silently dropped. Actor survives. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + ;; Pre-fill offset 0-31 with garbage (0xDE repeated) + (data (i32.const 0) "\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef\de\ad\be\ef") + + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param i32 i32) + ;; Send to garbage address at offset 0 + i32.const 0 ;; dest_ptr (garbage address from data segment) + i32.const 32 ;; payload_ptr + i32.const 1 ;; payload_len + call $send + ) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, ByteMessage(vec![42])).unwrap(); + rt.tick(); // send to garbage addr fails silently + + // Actor survives + rt.send_to(addr, ByteMessage(vec![99])).unwrap(); + rt.tick(); +} + +// ── Indirect call: guest uses call_indirect for handle logic ──────────────── + +#[test] +fn guest_using_call_indirect_works() { + // Guest uses a function table and call_indirect to invoke a function + // that calls send. Verifies table-based dispatch works in the sandbox. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (type $send_sig (func (param i32))) + + ;; A function that sends 1 byte from offset 200 using the dest at the param + (func $do_send (param $dest_ptr i32) + (i32.store8 (i32.const 200) (i32.const 55)) + local.get $dest_ptr + i32.const 200 + i32.const 1 + call $send + ) + + (table 1 funcref) + (elem (i32.const 0) $do_send) + + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Call the function at table index 0 via call_indirect + local.get $ptr + (call_indirect (type $send_sig) (i32.const 0)) + ) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"indirect")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("indirect call should deliver message"); + assert_eq!(received.0, vec![55]); +} + +// ── alloc returns 0 with len=0: subtle Ok(0) guard behavior ───────────────── + +#[test] +fn alloc_returns_zero_for_zero_length_message_succeeds() { + // The guard `Ok(0) if len > 0 => return` only triggers when len > 0. + // For a zero-length message, alloc returning 0 should fall through and + // handle(0, 0) should be called. This tests the subtle conditional. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $called (mut i32) (i32.const 0)) + + (func (export "alloc") (param i32) (result i32) + i32.const 0 ;; Always return 0 + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Mark that handle was called + (global.set $called (i32.const 1)) + ;; Write marker and send it + (i32.store8 (i32.const 200) (i32.const 77)) + ;; Use offset 100 as dest (will be zeroes = invalid addr, but that's fine) + i32.const 100 ;; dest_ptr (zeroes) + i32.const 200 ;; payload_ptr + i32.const 1 ;; payload_len + call $send + ) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // Send empty message — alloc returns 0, len == 0, so guard doesn't trigger. + // handle(0, 0) should be called. + rt.send_to(addr, ByteMessage(vec![])).unwrap(); + rt.tick(); + + // Then send a non-empty message — alloc returns 0, len > 0, guard triggers. + // handle should NOT be called. Actor survives. + rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); + rt.tick(); + + // Actor still alive + rt.send_to(addr, ByteMessage(vec![])).unwrap(); + rt.tick(); +} + +// ── Module with custom sections: should build successfully ────────────────── + +#[test] +fn module_with_custom_section_builds_and_works() { + // WASM modules can have custom sections. The builder should ignore them. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + (i32.store8 (i32.const 200) (i32.const 99)) + local.get $ptr + i32.const 200 + i32.const 1 + call $send + ) + (@custom "my_section" "hello") + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"custom")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("module with custom section should work"); + assert_eq!(received.0, vec![99]); +}