test: Cycle 53 — repeated build independence, stale memory, guest module fuzz, 3-deep calls

- repeated_build_same_bytes_independent: 3 actors from same bytes, stop one, others work
- guest_reads_stale_memory_region: reading uninitialized memory is safe
- prop_any_guest_module_processes_safely: echo/double/silent × random msg count
- three_level_nested_function_calls: inc→double_inc→transform chain (x+2)*3

All 228 tests pass (15 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 10:04:28 +00:00
parent af5e0644bc
commit 246f957721

View file

@ -7914,3 +7914,141 @@ fn guest_reads_i16_from_payload() {
assert_eq!(msg.0[0], 0x02, "low byte of 0x0102"); assert_eq!(msg.0[0], 0x02, "low byte of 0x0102");
assert_eq!(msg.0[1], 0x01, "high byte of 0x0102"); assert_eq!(msg.0[1], 0x01, "high byte of 0x0102");
} }
// ── Repeated build from same bytes yields independent actors ────────────────
#[test]
fn repeated_build_same_bytes_independent() {
let engine = SharedEngine::new().unwrap();
let wasm = guest_wasm("echo");
let rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
// Build 3 actors from exact same bytes
let a1 = rt.spawn(WasmActorBuilder::new(engine.clone(), wasm.clone()).build().unwrap()).unwrap();
let a2 = rt.spawn(WasmActorBuilder::new(engine.clone(), wasm.clone()).build().unwrap()).unwrap();
let a3 = rt.spawn(WasmActorBuilder::new(engine, wasm).build().unwrap()).unwrap();
// Stop a2 — a1 and a3 should still work
rt.stop_actor(a2);
rt.tick();
rt.tick();
rt.send_to(a1, framed_msg(inbox.addr(), b"a1")).unwrap();
rt.send_to(a3, framed_msg(inbox.addr(), b"a3")).unwrap();
rt.tick();
let mut msgs: Vec<Vec<u8>> = Vec::new();
while let Some(msg) = inbox.try_recv() {
msgs.push(msg.0);
}
msgs.sort();
assert_eq!(msgs, vec![b"a1".to_vec(), b"a3".to_vec()]);
}
// ── Guest writes beyond alloc region (within memory) — reads stale data ─────
#[test]
fn guest_reads_stale_memory_region() {
// Guest's alloc returns 4096, but the handle reads from offset 0 (outside alloc region).
// Memory at offset 0 was never written by the host for this message,
// but may have been written by a previous message. Tests that reading
// arbitrary memory is safe (no crash, just potentially stale data).
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)
;; Send 4 bytes from offset 0 (stale/zero memory)
(call $send (local.get $ptr) (i32.const 0) (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"trigger")).unwrap();
rt.tick();
let msg = inbox.try_recv().expect("reading stale memory is valid");
assert_eq!(msg.0.len(), 4, "should receive 4 bytes");
// Content is zero (fresh memory) — but we don't assert exact values
// since they could be anything in theory
}
// ── Property: any combination of guest modules processes without panic ──────
proptest! {
#[test]
fn prop_any_guest_module_processes_safely(
guest_idx in 0usize..3,
n_msgs in 0u8..8,
) {
let guests = ["echo", "double", "silent"];
let guest_name = guests[guest_idx];
let engine = SharedEngine::new().unwrap();
let actor = WasmActorBuilder::new(engine, guest_wasm(guest_name)).build().unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
for i in 0..n_msgs {
let _ = rt.send_to(addr, framed_msg(inbox.addr(), &[i]));
}
rt.tick();
while let Some(_) = inbox.try_recv() {}
}
}
// ── Guest with nested function calls (3 deep) ──────────────────────────────
#[test]
fn three_level_nested_function_calls() {
let wat = r#"
(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
(func $inc (param $x i32) (result i32)
(i32.add (local.get $x) (i32.const 1))
)
(func $double_inc (param $x i32) (result i32)
(call $inc (call $inc (local.get $x)))
)
(func $transform (param $x i32) (result i32)
(i32.mul (call $double_inc (local.get $x)) (i32.const 3))
)
(func (export "alloc") (param i32) (result i32) i32.const 4096)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Read first byte, transform: (x+2)*3
(i32.store8 (i32.const 200)
(call $transform
(i32.load8_u (i32.add (local.get $ptr) (i32.const 32)))
)
)
(call $send (local.get $ptr) (i32.const 200) (i32.const 1))
)
)
"#;
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();
// input=10: (10+2)*3 = 36
rt.send_to(addr, framed_msg(inbox.addr(), &[10])).unwrap();
rt.tick();
let msg = inbox.try_recv().unwrap();
assert_eq!(msg.0[0], 36, "(10+2)*3 = 36");
}