diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 769c5f3..317e6f5 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -3184,3 +3184,158 @@ fn two_watchers_both_notified_when_wasm_actor_dies() { assert_eq!(count_a.load(std::sync::atomic::Ordering::SeqCst), 1, "watcher A should be notified"); assert_eq!(count_b.load(std::sync::atomic::Ordering::SeqCst), 1, "watcher B should be notified"); } + +// ── Division by zero: WASM trap, actor survives ───────────────────────────── + +#[test] +fn guest_division_by_zero_traps_actor_survives() { + 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 256) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Division by zero is a trap in WASM + local.get $len + i32.const 0 + i32.div_u + drop + ) + ) + "#; + 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 addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, ByteMessage(vec![1])).unwrap(); + rt.tick(); // div by zero trap + + // Actor survives + rt.send_to(addr, ByteMessage(vec![2])).unwrap(); + rt.tick(); +} + +// ── Module with extra exports: globals and extra functions ────────────────── + +#[test] +fn module_with_extra_exports_builds_and_works() { + // Module exports extra globals and functions beyond the required ones. + // Builder should ignore them. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (global (export "version") i32 (i32.const 42)) + (global (export "magic") i64 (i64.const 12345)) + (func (export "alloc") (param i32) (result i32) i32.const 256) + (func (export "handle") (param $ptr i32) (param $len i32) + (i32.store8 (i32.const 200) (i32.const 7)) + local.get $ptr + i32.const 200 + i32.const 1 + call $send + ) + (func (export "extra_func") (param i32) (result i32) + local.get 0 + ) + ) + "#; + 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"extras")).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("module with extras should work"); + assert_eq!(received.0, vec![7]); +} + +// ── Send with dest_ptr=0, all zeroes in memory: valid but unroutable ──────── + +#[test] +fn send_with_all_zero_dest_from_uninitialized_memory() { + // Guest reads dest address from offset 500 (uninitialized, all zeros). + // The zero address isn't routable. ctx.send fails silently. + 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 256) + (func (export "handle") (param i32 i32) + ;; Read dest from uninitialized region (offset 500, all zeros) + i32.const 500 ;; dest_ptr + i32.const 32 ;; payload_ptr + i32.const 1 ;; payload_len + call $send + ) + ) + "#; + 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 addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, ByteMessage(vec![1, 2, 3])).unwrap(); + rt.tick(); // send to zero-address fails silently + + // Actor survives + rt.send_to(addr, ByteMessage(vec![4])).unwrap(); + rt.tick(); +} + +// ── Property: WASM actor survives any sequence of operations ──────────────── + +proptest! { + #[test] + fn prop_actor_survives_any_operation_sequence( + ops in proptest::collection::vec( + prop_oneof![ + Just("send"), + Just("empty"), + Just("large"), + ], + 1..20 + ) + ) { + 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(); + + for op in &ops { + match *op { + "send" => { + rt.send_to(addr, framed_msg(inbox.addr(), b"msg")).unwrap(); + } + "empty" => { + rt.send_to(addr, ByteMessage(vec![])).unwrap(); + } + "large" => { + rt.send_to(addr, ByteMessage(vec![0u8; 60000])).unwrap(); + } + _ => unreachable!(), + } + rt.tick(); + // Drain inbox + while inbox.try_recv().is_some() {} + } + + // Actor should still be alive + let result = rt.send_to(addr, ByteMessage(vec![99])); + prop_assert!(result.is_ok(), "actor must survive any operation sequence"); + } +}