test: Cycle 33 — outbox snapshot, grow-per-alloc, trap-after-send, offset-zero send

- guest_overwrites_payload_after_send_outbox_has_copy: outbox snapshots memory at send time
- alloc_grows_memory_returns_pointer_in_new_page: memory.grow per alloc, 5 messages through
- trap_after_successful_send_clears_outbox: trap discards all outbox entries including valid ones
- send_payload_at_memory_offset_zero: offset 0 is a valid payload location

All 143 tests pass. No new bugs found.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 09:38:50 +00:00
parent a4aaa9eeb0
commit 1dfc210bf0

View file

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