diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 582d4c5..b522cc8 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -1300,9 +1300,18 @@ fn wasm_actor_works_on_multi_worker_runtime() { // Use run() to drive the runtime on background threads let handle = rt.run().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(50)); - let received = inbox.try_recv().expect("wasm actor should echo on MT runtime"); + // Poll with retries — MT runtime timing is non-deterministic + let mut received = None; + for _ in 0..20 { + std::thread::sleep(std::time::Duration::from_millis(25)); + if let Some(msg) = inbox.try_recv() { + received = Some(msg); + break; + } + } + + let received = received.expect("wasm actor should echo on MT runtime"); assert_eq!(received.0, payload); handle.shutdown(); @@ -1841,9 +1850,18 @@ fn wasm_actors_communicate_across_threads() { rt.send_to(addr_a, msg_for_a).unwrap(); let handle = rt.run().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); - let received = inbox.try_recv().expect("cross-thread relay should deliver"); + // Poll with retries — MT runtime timing is non-deterministic + let mut received = None; + for _ in 0..20 { + std::thread::sleep(std::time::Duration::from_millis(25)); + if let Some(msg) = inbox.try_recv() { + received = Some(msg); + break; + } + } + + let received = received.expect("cross-thread relay should deliver"); assert_eq!(received.0, final_payload); handle.shutdown(); @@ -2229,3 +2247,157 @@ proptest! { prop_assert!(result.is_ok(), "actor must survive: alloc={alloc_val} trap={trap_handle} send_before={send_before_trap}"); } } + +// ── Stack overflow: deep recursion in handle ──────────────────────────────── + +#[test] +fn guest_stack_overflow_traps_actor_survives() { + // Guest handle calls itself recursively until stack overflow. + // Wasmtime should trap with a stack overflow error; actor must survive. + 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 $recurse (param $ptr i32) (param $len i32) + local.get $ptr + local.get $len + call $recurse + ) + (func (export "handle") (param $ptr i32) (param $len i32) + local.get $ptr + local.get $len + call $recurse + ) + ) + "#; + 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![1, 2, 3])).unwrap(); + rt.tick(); // stack overflow trap + + // Actor survives + rt.send_to(addr, ByteMessage(vec![4, 5, 6])).unwrap(); + rt.tick(); +} + +// ── Bulk memory: memory.fill and memory.copy ──────────────────────────────── + +#[test] +fn guest_using_bulk_memory_ops_works() { + // The engine enables bulk_memory. Guest uses memory.fill to write a + // pattern, then sends it. Verifies bulk memory operations work. + 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) + ;; Fill bytes [500..510] with value 42 using memory.fill + (memory.fill (i32.const 500) (i32.const 42) (i32.const 10)) + + ;; Send 10 bytes from [500..510] + local.get $ptr + i32.const 500 + i32.const 10 + 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"trigger")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("bulk memory fill should work"); + assert_eq!(received.0, vec![42u8; 10]); +} + +// ── Self-amplification: bounded by message budget ─────────────────────────── + +#[test] +fn self_amplification_bounded_by_budget_no_crash() { + // Guest sends 3 copies of the message back to itself. With budget=4 + // each tick processes at most 4 messages. Run for a few ticks — should + // not crash or OOM. + 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 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + (local $i i32) + (local.set $i (i32.const 0)) + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (i32.const 3))) + local.get $ptr + local.get $ptr + i32.const 33 + call $send + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ) + ) + "#; + let wasm = wat::parse_str(wat).unwrap(); + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wasm).build().unwrap(); + + let config = RuntimeConfig { + actor_message_budget: 4, + ..RuntimeConfig::default() + }; + let rt = Runtime::new(config); + let addr = rt.spawn(actor).unwrap(); + + // Initial seed: [self_addr | marker] + let mut seed = Vec::new(); + seed.extend_from_slice(&addr.0); + seed.push(0xFF); + rt.send_to(addr, ByteMessage(seed)).unwrap(); + + // Run for 5 ticks — should not crash + for _ in 0..5 { + rt.tick(); + } + + // Actor alive + rt.send_to(addr, ByteMessage(vec![0])).unwrap(); + rt.tick(); +} + +// ── Double stop: stopping an already-stopped actor ────────────────────────── + +#[test] +fn double_stop_is_idempotent() { + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, guest_wasm("silent")) + .build() + .unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + + rt.stop_actor(addr).unwrap(); + rt.tick(); + rt.tick(); + + // Second stop should fail gracefully (not panic) + let result = rt.stop_actor(addr); + assert!(result.is_err(), "stopping already-stopped actor should error"); +}