test: Cycle 32 — random payload fuzz, multi-page data, conditional fan-out, advancing alloc

- prop_random_payload_sizes_never_panic: random 0-8KB payloads never crash host
- multi_page_data_segments_persist: data segments initialized across 3 memory pages
- conditional_send_fan_out_based_on_payload: command byte controls 0/1/2x sends
- guest_with_advancing_allocator_handles_multiple_messages: proper bump alloc, 10 msgs in one tick

All 139 tests pass (9 property tests). No new bugs found.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 09:37:56 +00:00
parent 0fa9ff0e77
commit a4aaa9eeb0

View file

@ -4894,3 +4894,196 @@ fn start_function_that_succeeds_allows_normal_operation() {
let msg = inbox.try_recv().expect("should receive response after start"); let msg = inbox.try_recv().expect("should receive response after start");
assert_eq!(msg.0[0], 42, "start function should have initialized global to 42"); assert_eq!(msg.0[0], 42, "start function should have initialized global to 42");
} }
// ── Property: random payloads never cause host panic ────────────────────────
proptest! {
#[test]
fn prop_random_payload_sizes_never_panic(
payload in proptest::collection::vec(proptest::num::u8::ANY, 0..8192)
) {
let engine = SharedEngine::new().unwrap();
let actor = WasmActorBuilder::new(engine, guest_wasm("echo"))
.build()
.unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Send raw payload (not framed) — echo will try to use first 32 bytes as dest
// which will be random garbage. This should never crash the host.
rt.send_to(addr, ByteMessage(payload)).unwrap();
rt.tick();
// We don't care what happens — just that it doesn't panic
}
}
// ── Module with multiple memory pages and data segments ─────────────────────
#[test]
fn multi_page_data_segments_persist() {
// Module starts with 3 pages and has data segments in each page.
// Handle reads from each page to verify data segments initialized correctly.
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 3)
;; Data segment in page 0
(data (i32.const 100) "\AA\BB\CC")
;; Data segment in page 1 (offset 65536 + 100 = 65636)
(data (i32.const 65636) "\DD\EE\FF")
;; Data segment in page 2 (offset 131072 + 100 = 131172)
(data (i32.const 131172) "\11\22\33")
(func (export "alloc") (param i32) (result i32) i32.const 4096)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Copy 3 bytes from each page into response buffer at 200
(i32.store8 (i32.const 200) (i32.load8_u (i32.const 100)))
(i32.store8 (i32.const 201) (i32.load8_u (i32.const 65636)))
(i32.store8 (i32.const 202) (i32.load8_u (i32.const 131172)))
(call $send (local.get $ptr) (i32.const 200) (i32.const 3))
)
)
"#;
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"read-pages")).unwrap();
rt.tick();
let msg = inbox.try_recv().expect("should receive data from all pages");
assert_eq!(msg.0, vec![0xAA, 0xDD, 0x11], "data segments should be initialized across pages");
}
// ── Module that conditionally sends based on first payload byte ─────────────
#[test]
fn conditional_send_fan_out_based_on_payload() {
// Guest checks first payload byte:
// 0x01 → send to dest from msg
// 0x02 → send twice (double)
// anything else → don't send
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)
;; Read first payload byte (after 32-byte address header)
(if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 1))
(then
;; Send payload (skip first byte) once
(call $send
(local.get $ptr)
(i32.add (local.get $ptr) (i32.const 33))
(i32.sub (local.get $len) (i32.const 33))
)
)
)
(if (i32.eq (i32.load8_u (i32.add (local.get $ptr) (i32.const 32))) (i32.const 2))
(then
;; Send payload twice
(call $send
(local.get $ptr)
(i32.add (local.get $ptr) (i32.const 33))
(i32.sub (local.get $len) (i32.const 33))
)
(call $send
(local.get $ptr)
(i32.add (local.get $ptr) (i32.const 33))
(i32.sub (local.get $len) (i32.const 33))
)
)
)
)
)
"#;
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();
// Command 0x01: send once
rt.send_to(addr, framed_msg(inbox.addr(), &[0x01, b'X'])).unwrap();
// Command 0x02: send twice
rt.send_to(addr, framed_msg(inbox.addr(), &[0x02, b'Y'])).unwrap();
// Command 0xFF: no send
rt.send_to(addr, framed_msg(inbox.addr(), &[0xFF, b'Z'])).unwrap();
rt.tick();
let mut received = Vec::new();
while let Some(msg) = inbox.try_recv() {
received.push(msg.0.clone());
}
assert_eq!(received.len(), 3, "should get 1 + 2 + 0 = 3 messages");
assert_eq!(received[0], vec![b'X']);
assert_eq!(received[1], vec![b'Y']);
assert_eq!(received[2], vec![b'Y']);
}
// ── Guest allocator returns different offsets per call ───────────────────────
#[test]
fn guest_with_advancing_allocator_handles_multiple_messages() {
// Guest has a proper advancing bump allocator (not static offset).
// Each alloc call returns the next available slot. Verify messages
// don't overwrite each other when processed in the same tick.
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
(global $heap_ptr (mut i32) (i32.const 4096))
(func (export "alloc") (param $size i32) (result i32)
(local $ptr i32)
(local.set $ptr (global.get $heap_ptr))
;; Advance heap pointer (8-byte aligned)
(global.set $heap_ptr
(i32.and
(i32.add (i32.add (global.get $heap_ptr) (local.get $size)) (i32.const 7))
(i32.const -8)
)
)
(local.get $ptr)
)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Echo: send payload (after 32-byte header) to dest (first 32 bytes)
(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();
// Send 10 messages in one tick with distinct payloads
for i in 0u8..10 {
rt.send_to(addr, framed_msg(inbox.addr(), &[i; 16])).unwrap();
}
rt.tick();
let mut received = Vec::new();
while let Some(msg) = inbox.try_recv() {
received.push(msg.0.clone());
}
assert_eq!(received.len(), 10, "all 10 messages should be echoed");
for (i, payload) in received.iter().enumerate() {
assert_eq!(payload, &vec![i as u8; 16], "payload {i} should be intact");
}
}