From e1430750a820d68190314fef5f433aaee9b73227 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 10:21:00 +0000 Subject: [PATCH] =?UTF-8?q?test(wasm-actor):=20cycle=2064=20=E2=80=94=20ou?= =?UTF-8?q?tbox=20clear=20on=20OOB=20send,=20sign=20extend,=2060KB=20echo?= =?UTF-8?q?=20(279=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added 5 tests: send then OOB send clears outbox, i32.extend8_s instruction, double processes then stops cleanly, near-page payload echo (60KB), property test echo preserves arbitrary content. No new bugs found. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/wasm-actor/tests/wasm_actor.rs | 123 ++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 52bdfad..b1ffd60 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -9364,4 +9364,127 @@ fn stop_all_wasm_actors_in_runtime() { let _ = rt.send_to(*addr, ByteMessage(vec![])); } rt.tick(); // no panics +} + +// ── Cycle 64 ───────────────────────────────────────────────────────────────── + +// Guest calls send twice: once normally, once with dest beyond memory — second traps, +// but first send should still be in outbox (outbox cleared on trap) +#[test] +fn send_then_oob_send_clears_outbox() { + let wat = r#"(module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; First send: valid + (call $send (local.get $ptr) (i32.const 0) (i32.const 1)) + ;; Second send: dest_ptr = 65520, needs 32 bytes = 65552 > 65536 + ;; This traps! And the entire outbox should be cleared. + (call $send (i32.const 65520) (i32.const 0) (i32.const 1)) + ) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let addr = rt.spawn(actor).unwrap(); + + let mut msg = Vec::with_capacity(33); + msg.extend_from_slice(&inbox.addr().0); + msg.push(0x42); + rt.send_to(addr, ByteMessage(msg)).unwrap(); + rt.tick(); + + // The trap from second send should clear the outbox (Bug #3 fix) + // So the first send should NOT be delivered + assert!(inbox.try_recv().is_none(), "outbox cleared on trap — no message delivered"); +} + +// Guest with i32.extend8_s — sign extension instruction +#[test] +fn guest_uses_sign_extension() { + let wat = r#"(module + (memory (export "memory") 1) + (func (export "alloc") (param $len i32) (result i32) i32.const 1024) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Load byte, sign-extend, store as i32 + (i32.store (local.get $ptr) + (i32.extend8_s (i32.load8_u (local.get $ptr))) + ) + ) + )"#; + let engine = SharedEngine::new().unwrap(); + let actor = WasmActorBuilder::new(engine, wat::parse_str(wat).unwrap()) + .build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let addr = rt.spawn(actor).unwrap(); + rt.send_to(addr, ByteMessage(vec![0x80])).unwrap(); + rt.tick(); // 0x80 sign-extends to 0xFFFFFF80 — no trap +} + +// Double actor processes message then gets stopped — verified via inbox +#[test] +fn double_processes_then_stops_cleanly() { + let engine = SharedEngine::new().unwrap(); + let double = WasmActorBuilder::new(engine, guest_wasm("double")).build().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let d_addr = rt.spawn(double).unwrap(); + + // Send a message, then stop + rt.send_to(d_addr, framed_msg(inbox.addr(), b"Z")).unwrap(); + rt.tick(); + + let msgs: Vec<_> = std::iter::from_fn(|| inbox.try_recv().map(|m| m.0)).collect(); + assert_eq!(msgs.len(), 2, "double sends 2 copies"); + + rt.stop_actor(d_addr); + rt.tick(); + rt.tick(); + + // Send again — should be silently dropped (actor is dead) + let _ = rt.send_to(d_addr, framed_msg(inbox.addr(), b"after-stop")); + rt.tick(); + assert!(inbox.try_recv().is_none(), "no messages after stop"); +} + +// 64KB payload (entire page minus framing overhead) — stress the echo actor +#[test] +fn near_page_size_payload_echo() { + 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(); + + // 60000 bytes — large but within a single page + bump allocator space + let payload: Vec = (0..60000u32).map(|i| (i % 173) as u8).collect(); + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("large payload echo"); + assert_eq!(resp.0.len(), payload.len()); + assert_eq!(resp.0, payload); +} + +// Property: message content is always preserved by echo regardless of content +proptest! { + #[test] + fn prop_echo_preserves_arbitrary_content( + payload in proptest::collection::vec(0u8..=255, 1..4096) + ) { + 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(); + + rt.send_to(addr, framed_msg(inbox.addr(), &payload)).unwrap(); + rt.tick(); + + let resp = inbox.try_recv().expect("echo should respond"); + assert_eq!(resp.0, payload, "echo must preserve content exactly"); + } } \ No newline at end of file