diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 1d01b4b..6564db0 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -5087,3 +5087,171 @@ fn guest_with_advancing_allocator_handles_multiple_messages() { assert_eq!(payload, &vec![i as u8; 16], "payload {i} should be intact"); } } + +// ── Guest writes to memory after send — outbox snapshot safety ────────────── + +#[test] +fn guest_overwrites_payload_after_send_outbox_has_copy() { + // Guest sends a message, then overwrites the same memory region. + // The outbox should hold a snapshot of the data at the time of send, + // not a reference to the mutable linear memory. + 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) + ;; Write "AAAA" at offset 200 + (i32.store (i32.const 200) (i32.const 0x41414141)) + ;; Send "AAAA" (4 bytes) + (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) + ;; Overwrite the same region with "BBBB" + (i32.store (i32.const 200) (i32.const 0x42424242)) + ;; Send "BBBB" (4 bytes) + (call $send (local.get $ptr) (i32.const 200) (i32.const 4)) + ) + ) + "#; + 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"snapshot")).unwrap(); + rt.tick(); + + let msg1 = inbox.try_recv().expect("first send should deliver"); + let msg2 = inbox.try_recv().expect("second send should deliver"); + assert_eq!(msg1.0, b"AAAA", "first send should have original bytes"); + assert_eq!(msg2.0, b"BBBB", "second send should have overwritten bytes"); +} + +// ── Alloc that does memory.grow and returns pointer in new page ───────────── + +#[test] +fn alloc_grows_memory_returns_pointer_in_new_page() { + // Guest's alloc grows memory by 1 page and returns start of new page. + // Each call to alloc adds a page and returns a fresh region. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param $size i32) (result i32) + (local $old_pages i32) + ;; Grow memory by 1 page, return start of new page + (local.set $old_pages (memory.grow (i32.const 1))) + ;; If grow failed (returned -1), return -1 + (if (result i32) (i32.eq (local.get $old_pages) (i32.const -1)) + (then (i32.const -1)) + (else (i32.mul (local.get $old_pages) (i32.const 65536))) + ) + ) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Echo payload to dest + (call $send + (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32)) + ) + ) + ) + "#; + 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(); + + // Each message causes a memory.grow, so memory increases: 1→2→3→4→5 pages + for i in 0u8..5 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + rt.tick(); + + let mut received = Vec::new(); + while let Some(msg) = inbox.try_recv() { + received.push(msg.0[0]); + } + assert_eq!(received, vec![0, 1, 2, 3, 4], "all messages should echo through grown pages"); +} + +// ── Handle that traps after successful send — outbox cleared ──────────────── + +#[test] +fn trap_after_successful_send_clears_outbox() { + // Guest does a valid send, then traps. The outbox should be cleared + // and the send should NOT be delivered. + 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) + ;; Write payload + (i32.store8 (i32.const 200) (i32.const 99)) + ;; Valid send + (call $send (local.get $ptr) (i32.const 200) (i32.const 1)) + ;; Now trap + 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(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"doomed")).unwrap(); + rt.tick(); + + // Outbox should be cleared by the trap, so nothing delivered + assert!(inbox.try_recv().is_none(), "trap should clear all outbox sends"); + + // Actor survives — send another, still traps + rt.send_to(addr, framed_msg(inbox.addr(), b"also-doomed")).unwrap(); + rt.tick(); + assert!(inbox.try_recv().is_none(), "actor survives but always traps"); +} + +// ── Send from WAT module using payload at start of memory (offset 0) ──────── + +#[test] +fn send_payload_at_memory_offset_zero() { + // Guest writes payload at offset 0 and sends from there. + // Tests that offset 0 is a valid payload location. + 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) + ;; Write "OK" at offset 0 + (i32.store8 (i32.const 0) (i32.const 79)) ;; 'O' + (i32.store8 (i32.const 1) (i32.const 75)) ;; 'K' + ;; Send from offset 0 with len 2 + (call $send (local.get $ptr) (i32.const 0) (i32.const 2)) + ) + ) + "#; + 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 msg = inbox.try_recv().expect("send from offset 0 should work"); + assert_eq!(msg.0, b"OK"); +}