diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index c597ff5..f3a8d44 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -7913,4 +7913,142 @@ fn guest_reads_i16_from_payload() { // i32.load16_u reads LE: 0x0102 assert_eq!(msg.0[0], 0x02, "low 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::().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::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::().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::().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::().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"); } \ No newline at end of file