From 55a8ddb97a06b93a6d304171fba79278c7205e47 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:36:13 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2030=20=E2=80=94=20reftype=20reje?= =?UTF-8?q?ction,=20trap=20isolation,=20negative=20alloc,=20global=20count?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reference_types_module_rejected: externref table rejected by sandboxed engine - trapping_actor_does_not_affect_sibling: trap in actor 1 doesn't corrupt actor 2 - alloc_returns_negative_for_nonzero_drops_message: negative ptr gracefully drops - global_counter_accumulates_across_messages: mutable global state persists across 5 calls All 131 tests pass. No new bugs found. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 143 +++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 5bde586..9277029 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -4367,7 +4367,7 @@ fn pingpong_wasm_echoes_bounded_by_budget() { let rt = Runtime::new(config); let inbox = rt.new_inbox::().unwrap(); let a1 = rt.spawn(echo1).unwrap(); - let a2 = rt.spawn(echo2).unwrap(); + let _a2 = rt.spawn(echo2).unwrap(); // Seed: tell actor 1 to echo to actor 2, with addr of actor 1 as payload // so actor 2's reply goes back to actor 1 (creating a loop). @@ -4626,3 +4626,144 @@ fn send_dest_one_past_memory_boundary_traps() { // This time no OOB send, but the module always tries the OOB send, so still trapped assert!(inbox.try_recv().is_none(), "same module always traps"); } + +// ── Reference types module rejected by engine ─────────────────────────────── + +#[test] +fn reference_types_module_rejected() { + // Module uses externref (reference types disabled in SharedEngine). + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "alloc") (param i32) (result i32) i32.const 0) + (func (export "handle") (param i32 i32)) + (table 1 externref) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, wasm).build(); + assert!(result.is_err(), "reference types should be rejected by sandboxed engine"); +} + +// ── Two actors, one traps always, one works — independent store isolation ─── + +#[test] +fn trapping_actor_does_not_affect_sibling() { + // Actor 1 always traps in handle. Actor 2 echos normally. + // Verify trap in actor 1 doesn't corrupt/poison actor 2. + let trap_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 4096) + (func (export "handle") (param i32 i32) + unreachable + ) + ) + "#; + let trap_wasm = wat::parse_str(trap_wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let trapper = WasmActorBuilder::new(engine.clone(), trap_wasm).build().unwrap(); + let echoer = WasmActorBuilder::new(engine, guest_wasm("echo")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let trap_addr = rt.spawn(trapper).unwrap(); + let echo_addr = rt.spawn(echoer).unwrap(); + + // Send to both in the same tick + rt.send_to(trap_addr, framed_msg(inbox.addr(), b"trap-this")).unwrap(); + rt.send_to(echo_addr, framed_msg(inbox.addr(), b"echo-this")).unwrap(); + rt.tick(); + + // Only echo actor should deliver + let mut msgs = Vec::new(); + while let Some(msg) = inbox.try_recv() { + msgs.push(msg.0); + } + assert_eq!(msgs.len(), 1, "only echo actor should deliver"); + assert_eq!(&msgs[0], b"echo-this"); +} + +// ── Alloc returns negative for non-zero len — message dropped gracefully ──── + +#[test] +fn alloc_returns_negative_for_nonzero_drops_message() { + // Guest alloc always returns -42 regardless of input. + 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 -42) + (func (export "handle") (param i32 i32) + ;; Should never be called because alloc returns negative + unreachable + ) + ) + "#; + 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(); + + // Send several messages — all should be silently dropped + for i in 0..5 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + rt.tick(); + + assert!(inbox.try_recv().is_none(), "negative alloc should drop messages"); + + // Actor should still be alive — send another message, still dropped + rt.send_to(addr, framed_msg(inbox.addr(), &[99])).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "actor alive but still drops (negative alloc)"); +} + +// ── WASM actor with global state accumulates across messages ──────────────── + +#[test] +fn global_counter_accumulates_across_messages() { + // Guest has a mutable global counter. Each handle call increments it. + // The response payload includes the counter value. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global $counter (mut i32) (i32.const 0)) + + (func (export "alloc") (param i32) (result i32) i32.const 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Increment counter + (global.set $counter (i32.add (global.get $counter) (i32.const 1))) + ;; Write counter value as single byte at offset 200 + (i32.store8 (i32.const 200) (global.get $counter)) + ;; Send counter value back to sender (first 32 bytes of msg) + (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) + ) + ) + "#; + 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(); + + // Send 5 messages across 5 ticks + for _ in 0..5 { + rt.send_to(addr, framed_msg(inbox.addr(), b"inc")).unwrap(); + rt.tick(); + } + + let mut counter_values = Vec::new(); + while let Some(msg) = inbox.try_recv() { + counter_values.push(msg.0[0]); + } + assert_eq!(counter_values, vec![1, 2, 3, 4, 5], "global state should persist across handle calls"); +}