From 4a0cde4b41f8798e9d2d463b575d94df59de0c5a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:11:00 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2015=20=E2=80=94=20outbox=20flood?= =?UTF-8?q?=20(1000=20msgs),=20mixed=20actor=20cleanup,=20i32::MAX=20alloc?= =?UTF-8?q?,=20size-varied=20fuzz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 0bddffc..3f9abfb 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -2736,3 +2736,142 @@ fn byte_message_traits() { let debug_str = format!("{:?}", msg1); assert!(debug_str.contains("ByteMessage")); } + +// ── Malicious guest: massive outbox (memory exhaustion defense) ───────────── + +#[test] +fn guest_sending_1000_messages_in_one_handle_all_delivered() { + // A malicious guest could flood the outbox with thousands of messages. + // The host should handle this without crashing. Each message is small. + 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) + (local $i i32) + (local.set $i (i32.const 0)) + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (i32.const 1000))) + (call $send (local.get $ptr) (i32.const 32) (i32.const 0)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ) + ) + "#; + 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"flood")).unwrap(); + rt.tick(); + + let mut count = 0; + while inbox.try_recv().is_some() { + count += 1; + } + assert_eq!(count, 1000, "all 1000 messages should be delivered"); +} + +// ── Interleaved message types: ByteMessage + watch in same tick ───────────── + +#[test] +fn wasm_actor_processes_messages_and_receives_watch_notification() { + // WASM echo actor processes a message and then receives a watch + // notification for a stopped actor — both in a short sequence. + let engine = SharedEngine::new().unwrap(); + let echo = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) + .build() + .unwrap(); + let silent = WasmActorBuilder::new(engine, guest_wasm("silent")) + .build() + .unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let echo_addr = rt.spawn(echo).unwrap(); + let silent_addr = rt.spawn(silent).unwrap(); + rt.tick(); // let actors initialize + + // Echo processes a message + rt.send_to(echo_addr, framed_msg(inbox.addr(), b"before-death")).unwrap(); + rt.tick(); + let received = inbox.try_recv().expect("echo should work before death notification"); + assert_eq!(received.0, b"before-death"); + + // Stop silent actor — echo doesn't watch it, so no notification expected + // But this tests that the runtime handles mixed actor types during cleanup + rt.stop_actor(silent_addr).unwrap(); + rt.tick(); + rt.tick(); + + // Echo still works after another actor died + rt.send_to(echo_addr, framed_msg(inbox.addr(), b"after-death")).unwrap(); + rt.tick(); + let received = inbox.try_recv().expect("echo should work after other actor dies"); + assert_eq!(received.0, b"after-death"); +} + +// ── Alloc returns i32::MAX: maximum positive value ────────────────────────── + +#[test] +fn alloc_returns_i32_max_drops_message_actor_survives() { + // alloc returns i32::MAX (2147483647). (ptr as usize).saturating_add(len) + // produces a huge value, bounds check rejects. 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 2147483647 + ) + (func (export "handle") (param i32 i32)) + ) + "#; + 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(); + + // Actor survives + rt.send_to(addr, ByteMessage(vec![4])).unwrap(); + rt.tick(); +} + +// ── Property: echo preserves message integrity under varied sizes ─────────── + +proptest! { + #[test] + fn prop_echo_preserves_payloads_of_varied_sizes(size in 1usize..2000) { + // Messages of varying sizes should echo perfectly through the pipeline. + // Tests allocation alignment and copy correctness at many sizes. + 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(); + + let payload: Vec = (0..size).map(|i| (i % 256) as u8).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let received = inbox.try_recv().expect("echo should return payload"); + prop_assert_eq!(received.0, payload); + } +}