test: Cycle 13 — SIMD rejection, garbage address, call_indirect, alloc-zero-len, custom sections

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 09:08:10 +00:00
parent 84911869c8
commit 186af06ec2

View file

@ -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::<ByteMessage>().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::<ByteMessage>().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]);
}