diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index afa613f..d20957e 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -3980,3 +3980,132 @@ fn wasm_actor_is_send() { fn assert_send() {} assert_send::(); } + +// ── Guest writes to alloc pointer region before sending ───────────────────── + +#[test] +fn guest_modifies_received_message_before_echoing() { + // Guest receives a message, XORs each payload byte with 0xFF, then + // echoes the modified payload. Verifies that the guest can mutate + // linear memory and the modified data is what gets sent. + 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) + (local $i i32) + (local $payload_start i32) + (local $payload_len i32) + + ;; payload starts at ptr+32, length is len-32 + (local.set $payload_start (i32.add (local.get $ptr) (i32.const 32))) + (local.set $payload_len (i32.sub (local.get $len) (i32.const 32))) + + ;; Skip if no payload + (br_if 0 (i32.lt_s (local.get $payload_len) (i32.const 1))) + + ;; XOR each byte with 0xFF + (local.set $i (i32.const 0)) + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (local.get $payload_len))) + (i32.store8 + (i32.add (local.get $payload_start) (local.get $i)) + (i32.xor + (i32.load8_u (i32.add (local.get $payload_start) (local.get $i))) + (i32.const 255) + ) + ) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + + ;; Send modified payload + local.get $ptr + local.get $payload_start + local.get $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 inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + let payload = vec![0x00, 0x0F, 0xF0, 0xFF]; + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("XOR transform should send"); + let expected: Vec = payload.iter().map(|b| b ^ 0xFF).collect(); + assert_eq!(received.0, expected, "payload should be XOR'd with 0xFF"); +} + +// ── Stop and re-spawn at same conceptual slot ─────────────────────────────── + +#[test] +fn stop_and_respawn_same_type_repeatedly() { + // Stop and respawn the same type of WASM actor 5 times. + // Each new instance should work independently. + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("echo"); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + for round in 0u8..5 { + let actor = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()) + .build().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + rt.send_to(addr, framed_msg(inbox.addr(), &[round])).unwrap(); + rt.tick(); + let received = inbox.try_recv().expect("respawned actor should echo"); + assert_eq!(received.0, vec![round]); + + rt.stop_actor(addr).unwrap(); + rt.tick(); + rt.tick(); + } +} + +// ── WASM actor watches another WASM actor ─────────────────────────────────── +// Note: WasmActor doesn't implement on_actor_exit, so watch notifications +// are received but unhandled (default no-op). The important thing is no crash. + +#[test] +fn wasm_actor_watching_another_wasm_actor_doesnt_crash() { + // Two WASM actors. We can't make one watch the other through the WASM + // ABI (ctx.watch isn't exposed to guests). But we can have a native + // watcher confirm the runtime handles WASM actors in the watch system. + // (Already covered by native_watcher_notified_when_wasm_actor_stops, + // but let's verify with two WASM actors dying in sequence.) + let engine = SharedEngine::new().unwrap(); + let wasm_bytes = guest_wasm("silent"); + + let actor_a = WasmActorBuilder::new(engine.clone(), wasm_bytes.clone()).build().unwrap(); + let actor_b = WasmActorBuilder::new(engine, wasm_bytes).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let addr_a = rt.spawn(actor_a).unwrap(); + let addr_b = rt.spawn(actor_b).unwrap(); + rt.tick(); + + // Stop both in sequence + rt.stop_actor(addr_a).unwrap(); + rt.tick(); + rt.tick(); + + rt.stop_actor(addr_b).unwrap(); + rt.tick(); + rt.tick(); + + // Both gone, no crash + assert!(rt.send_to(addr_a, ByteMessage(vec![1])).is_err()); + assert!(rt.send_to(addr_b, ByteMessage(vec![1])).is_err()); +}