diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 25e76af..1d01b4b 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -4894,3 +4894,196 @@ fn start_function_that_succeeds_allows_normal_operation() { 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"); } + +// ── 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::().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::().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::().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::().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"); + } +}