From 68952517eace972163a3a608a5afdcc79ede4194 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:42:02 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2035=20=E2=80=94=20loop=20sum=20c?= =?UTF-8?q?ompute,=20dest=5Fptr=3D0,=20stop-with-pending,=20truncated=20WA?= =?UTF-8?q?T=20fuzz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handle_with_loop_computes_sum: iterative byte sum via wasm loop/br, u8 overflow wrapping - send_with_dest_ptr_zero_reads_from_memory_start: dest_ptr=0 is a valid location - stop_actor_with_pending_messages_no_crash: stop before tick with 10 queued messages - prop_truncated_wat_always_produces_error: any truncated WASM bytes always fail to build All 150 tests pass (10 property tests). No new bugs found. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 152 ++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index c4c07a4..b2297dd 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -5381,3 +5381,155 @@ fn handle_with_if_else_branching() { assert_eq!(msg1.0, b"SML"); assert_eq!(msg2.0, b"BIG"); } + +// ── Module with loop/br — iterative computation in handle ─────────────────── + +#[test] +fn handle_with_loop_computes_sum() { + // Guest sums all payload bytes using a loop and sends the sum as a single byte. + 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 $sum 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))) + (local.set $i (i32.const 0)) + (local.set $sum (i32.const 0)) + ;; Sum loop + (block $break + (loop $loop + (br_if $break (i32.ge_u (local.get $i) (local.get $payload_len))) + (local.set $sum + (i32.add + (local.get $sum) + (i32.load8_u (i32.add (local.get $payload_start) (local.get $i))) + ) + ) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ;; Write sum (truncated to u8) at offset 200 + (i32.store8 (i32.const 200) (local.get $sum)) + (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(); + + // Sum of [1, 2, 3, 4, 5] = 15 + rt.send_to(addr, framed_msg(inbox.addr(), &[1, 2, 3, 4, 5])).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().expect("should receive sum"); + assert_eq!(msg.0[0], 15, "sum of [1,2,3,4,5] should be 15"); + + // Sum of [100, 100, 56] = 256 → truncated to 0 (u8 overflow) + rt.send_to(addr, framed_msg(inbox.addr(), &[100, 100, 56])).unwrap(); + rt.tick(); + let msg2 = inbox.try_recv().expect("should receive truncated sum"); + assert_eq!(msg2.0[0], 0, "256 truncated to u8 wraps to 0"); +} + +// ── Send import with dest_ptr = 0 (valid, reads from start of memory) ────── + +#[test] +fn send_with_dest_ptr_zero_reads_from_memory_start() { + // Guest copies the address to offset 0, then sends with dest_ptr=0. + 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) + ;; Copy 32-byte address from message to offset 0 + (memory.copy (i32.const 0) (local.get $ptr) (i32.const 32)) + ;; Write payload at offset 300 + (i32.store8 (i32.const 300) (i32.const 42)) + ;; Send with dest_ptr=0 + (call $send (i32.const 0) (i32.const 300) (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(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"zero-dest")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().expect("dest_ptr=0 should be valid"); + assert_eq!(msg.0, vec![42]); +} + +// ── Wasm actors survive being stopped while message in flight ─────────────── + +#[test] +fn stop_actor_with_pending_messages_no_crash() { + 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(); + + // Send messages, then stop before tick + for i in 0u8..10 { + rt.send_to(addr, framed_msg(inbox.addr(), &[i])).unwrap(); + } + rt.stop_actor(addr); + rt.tick(); + + // The actor may or may not have processed some messages before being stopped. + // The important thing is no crash/panic. + let mut count = 0; + while let Some(_) = inbox.try_recv() { + count += 1; + } + // Count can be 0..=10, we just verify no panic + assert!(count <= 10, "at most 10 messages should be received"); +} + +// ── Property: build from any subset of valid WAT produces valid error ─────── + +proptest! { + #[test] + fn prop_truncated_wat_always_produces_error( + len in 0usize..200 + ) { + let full_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 0) + (func (export "handle") (param i32 i32)) + ) + "#; + let wasm_full = wat::parse_str(full_wat).unwrap(); + // Truncate the WASM bytes + let truncated: Vec = wasm_full.iter().take(len).copied().collect(); + if truncated.len() < wasm_full.len() { + let engine = SharedEngine::new().unwrap(); + let result = WasmActorBuilder::new(engine, truncated).build(); + // Truncated should always fail (unless we took all bytes) + assert!(result.is_err(), "truncated WASM should fail to build"); + } + } +}