From 8ae620ebbb8fa17db7e42b1713265339a4fe16cb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 08:57:50 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=208=20=E2=80=94=20off-by-one=20bo?= =?UTF-8?q?undary,=20spawn-stop=20lifecycle,=203-hop=20+=2010-hop=20chain,?= =?UTF-8?q?=20memory.grow=20exhaust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 181 ++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 22c4c29..a172b58 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -1555,3 +1555,184 @@ fn exact_fit_allocation_at_memory_boundary_succeeds() { rt.send_to(addr, ByteMessage(vec![1u8; 10])).unwrap(); rt.tick(); } + +// ── Off-by-one: alloc returns exactly memory size ──────────────────────────── + +#[test] +fn alloc_returns_exactly_memory_size_drops_message() { + // alloc returns 65536 (exactly the size of 1-page memory). + // Any non-zero length message means end > mem.len(), so it should be dropped. + // For a zero-length message, ptr=65536, end=65536, which equals mem.len() + // so end > mem.len() is false — that path technically works (no-op write). + 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 65536 ;; exactly at memory boundary + ) + (func (export "handle") (param i32 i32)) + ) + "#; + 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(); + + // Non-zero message: end = 65536 + 5 = 65541 > 65536 → dropped + rt.send_to(addr, ByteMessage(vec![0u8; 5])).unwrap(); + rt.tick(); + + // Actor survives + rt.send_to(addr, ByteMessage(vec![1u8; 5])).unwrap(); + rt.tick(); +} + +// ── Lifecycle: spawn and immediately stop without processing messages ──────── + +#[test] +fn spawn_and_stop_without_messages_is_clean() { + // WASM actor spawned, immediately stopped, never processes a message. + // The wasmtime Store should be dropped cleanly. + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("echo")) + .build() + .unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + // Stop immediately, no messages sent + rt.stop_actor(addr).unwrap(); + rt.tick(); // process stop + rt.tick(); // cleanup_dead + + // Actor is gone + let result = rt.send_to(addr, ByteMessage(vec![1])); + assert!(result.is_err(), "stopped actor should reject messages"); +} + +// ── 3-hop relay: WASM A → WASM B → WASM C → inbox ────────────────────────── + +#[test] +fn three_hop_wasm_relay_delivers_final_payload() { + // Three echo actors in sequence: A echoes to B, B echoes to C, C echoes to inbox. + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("echo"); + + let actor_a = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()).build().unwrap(); + let actor_b = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()).build().unwrap(); + let actor_c = WasmActorBuilder::new(engine, wasm_bytes).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let addr_a = rt.spawn(actor_a).unwrap(); + let addr_b = rt.spawn(actor_b).unwrap(); + let addr_c = rt.spawn(actor_c).unwrap(); + + // Build nested framed message: A sends to B, B sends to C, C sends to inbox + let final_payload = b"3-hops"; + let msg_for_c = framed_msg(inbox.addr(), final_payload); + let msg_for_b = framed_msg(&addr_c, &msg_for_c.0); + let msg_for_a = framed_msg(&addr_b, &msg_for_b.0); + + rt.send_to(addr_a, msg_for_a).unwrap(); + rt.tick(); // A → B + rt.tick(); // B → C + rt.tick(); // C → inbox + + let received = inbox.try_recv().expect("3-hop relay should deliver"); + assert_eq!(received.0, final_payload); +} + +// ── memory.grow exhaustion: guest grows until failure ──────────────────────── + +#[test] +fn memory_grow_until_failure_actor_survives() { + // Guest calls memory.grow repeatedly until it returns -1 (failure). + // The actor should survive and the send should still work using + // memory from before the failed grow. + 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) + (local $result i32) + ;; Grow memory repeatedly until failure + (block $done + (loop $grow + (local.set $result (memory.grow (i32.const 100))) + (br_if $done (i32.eq (local.get $result) (i32.const -1))) + (br $grow) + ) + ) + ;; After grow failure, write marker and send from original page + (i32.store8 (i32.const 200) (i32.const 77)) + local.get $ptr ;; dest_ptr + 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 inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"grow-exhaust")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("actor should work after grow exhaustion"); + assert_eq!(received.0, vec![77]); +} + +// ── Stress: many WASM actors in a chain ────────────────────────────────────── + +#[test] +fn ten_wasm_actors_chain_relay() { + // 10 echo actors in a chain: actor[0]→actor[1]→...→actor[9]→inbox. + // Tests that many WASM actors coexist and messages propagate through them. + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("echo"); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let mut addrs = Vec::new(); + for _ in 0..10 { + let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) + .build() + .unwrap(); + addrs.push(rt.spawn(actor).unwrap()); + } + + // Build nested framed message from the inside out: + // actor[9] receives [inbox_addr | final_payload] → echoes final_payload to inbox + // actor[8] receives [addr[9] | msg_for_9] → echoes msg_for_9 to actor[9] + // ... + // actor[0] receives [addr[1] | msg_for_1] → echoes msg_for_1 to actor[1] + let final_payload = b"chain-10"; + let mut msg = framed_msg(inbox.addr(), final_payload); + for addr in addrs[1..].iter().rev() { + msg = framed_msg(addr, &msg.0); + } + + rt.send_to(addrs[0], msg).unwrap(); + for _ in 0..10 { + rt.tick(); + } + + let received = inbox.try_recv().expect("10-actor chain should deliver"); + assert_eq!(received.0, final_payload); +}