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:
parent
a4aaa9eeb0
commit
1dfc210bf0
1 changed files with 168 additions and 0 deletions
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue