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