From 5c42cad8da8dc614a76bc0312cfc3443a4c079ed Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 09:48:26 +0000 Subject: [PATCH] =?UTF-8?q?test:=20Cycle=2040=20=E2=80=94=20pre-filled=20m?= =?UTF-8?q?emory,=20engine=20clone,=20i64=20ops,=20200-actor=20stress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - guest_start_function_modifies_alloc_region: host overwrites pre-filled 0xFF memory - engine_clone_is_same_engine: cloned engine produces working actors - handle_uses_i64_operations: i64 arithmetic with i32.wrap_i64 - two_hundred_actors_from_same_engine: 200 actors all process one message - Updated history.md with Cycles 27-39 All 170 tests pass. No new bugs found. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- CLAUDE/notes/history.md | 194 ++++++++++++++++++++++++++ crates/wasm-actor/tests/wasm_actor.rs | 136 ++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 CLAUDE/notes/history.md diff --git a/CLAUDE/notes/history.md b/CLAUDE/notes/history.md new file mode 100644 index 0000000..c170cb4 --- /dev/null +++ b/CLAUDE/notes/history.md @@ -0,0 +1,194 @@ +# Cycle History (append-only) + +## Cycle 0 — Research +- Read WASM runner implementation end-to-end (5 source files, 3 guest modules, 11 tests) +- Wrote feature_map.md, constraints.md, research_synthesis.md (15 ranked test scenarios) +- Found potential bug P0-1: missing bounds check in actor.rs:44-46 (copy_from_slice OOB) +- Built guest WASM modules, verified all 11 baseline tests pass + +## Cycle 1 — P0/P1/P2 Tests + 2 Bug Fixes +- Wrote 12 new tests (P0-1 through P2-1), total now 23 +- Bug #1 fixed: actor.rs bounds check — alloc OOB ptr caused permanent actor poisoning +- Bug #2 found+fixed: worker.rs StopSignal didn't push to deaths — watchers never notified for stop_actor() +- All 23 WASM actor tests + 157 core tests pass + +## Cycle 2 — P2-2 + P3 Property Tests +- Added multi-worker runtime test (P2-2) — Send safety of wasmtime Store verified +- Added 2 property-based tests (proptest): echo round-trip + double 2x invariant +- All 26 WASM actor tests pass, all scenarios from research_synthesis.md complete + +## Cycle 3 — Deep Dive: Stale Outbox + Edge Cases +- Bug #3 found+fixed: outbox not cleared on handle trap — stale sends leaked into next call +- Added 4 new tests: stale outbox leak, invalid WASM bytes, zero-length payload send, multiple sequential traps +- All 30 tests pass + +## Cycle 4 — Start Trap, Self-Send, Amplification, Overlap +- Added 4 more tests: trapping start function, self-send feedback loop, 10x amplification, overlapping send regions +- No new bugs found (all 4 pass) +- All 34 tests pass + +## Cycle 5 — Type Mismatch, State Persistence, Dynamic Spawn +- Added 3 tests: type mismatch handling, guest mutable state persistence, dynamic WASM spawn from handler +- No new bugs found +- All 37 tests pass + +## Cycle 6 — Bounded Mailbox + Alloc Fuzzing +- Added bounded mailbox backpressure test (DropNewest policy) +- Added property test: any alloc return value (-100..70000) never kills actor +- All 39 tests pass + +## Cycle 7 — Alloc Trap, memory.grow, Send Overflow, Exact-Fit +- Added 4 tests: alloc trapping (store recovery), memory.grow during handle, send with dest_ptr=i32::MAX overflow, exact-fit allocation at boundary +- No new bugs found — all edge cases handled correctly +- All 43 tests pass + +## Cycle 8 — Off-By-One, Lifecycle, Chains, Grow Exhaust +- Added 5 tests: alloc returns exactly memory size, spawn-stop without messages, 3-hop relay, 10-hop chain relay, memory.grow until failure +- No new bugs found +- All 48 tests pass + +## Cycle 9 — Send Overflow/Boundary, Cross-Thread, Fuzz Send Args +- Added 4 tests: payload range overflow, payload at exact memory end, cross-thread WASM relay, property test fuzzing all send args +- No new bugs found +- All 52 tests pass + +## Cycle 10 — Multi-Dest Sends, Outbox Copy, Large Payload, Alloc-with-Grow +- Added 4 tests: two destinations in one handle, memory overwrite after send (outbox copy safety), large payload, alloc that grows memory returns new region pointer +- No new bugs found +- All 56 tests pass + +## Cycle 11 — Message Budget, Data Segments, Outbox Isolation, Combined Fuzz +- Added 4 tests: actor_message_budget fairness, data segment initialized memory, two-actor outbox isolation, combined alloc+trap+send property fuzz +- No new bugs found — prop_random_module_behavior_never_crashes is strongest general safety property +- All 60 tests pass + +## Cycle 12 — Stack Overflow, Bulk Memory, Self-Amplification, Double Stop +- Added 4 tests: infinite recursion trap, bulk memory.fill, self-amplification bounded by budget, double stop idempotency +- Fixed 2 flaky MT tests (poll with retry loop instead of fixed sleep) +- All 64 tests pass + +## Cycle 13 — SIMD Rejection, Garbage Address, call_indirect, Custom Sections +- Added 5 tests: SIMD module rejected by sandboxed engine, garbage address bytes silently dropped, call_indirect dispatch, alloc returning 0 for zero-length message, custom section tolerance +- Subtle Ok(0) guard behavior verified: alloc returns 0 + len=0 falls through to handle(0,0) +- All 69 tests pass + +## Cycle 14 — Multi-Engine, Error Formatting, Rapid Lifecycle, Stop-Send Race +- Added 6 tests: actors from different engines coexist, error Display formatting, rapid spawn-process-stop (20 iterations), stop-send race, SharedEngine Debug, ByteMessage traits +- No new bugs found +- All 75 tests pass + +## Cycle 15 — Outbox Flood, Mixed Cleanup, i32::MAX Alloc, Size Fuzz +- Added 4 tests: 1000-message outbox flood, interleaved WASM+native actor cleanup, alloc returning i32::MAX, property test for varied payload sizes +- No new bugs found +- All 79 tests pass (6 property tests) + +## Cycle 16 — Spawn+Send Same Tick, 21-Actor Mixed Runtime, Alternating Alloc +- Added 3 tests: message delivery on spawn tick, 20 native + 1 WASM actor mixed runtime, alloc alternating -1/256 +- No new bugs found +- All 82 tests pass + +## Cycle 17 — Truncated WASM, No-Import Module, 4-Thread Stress, i32::MIN, Dual Watcher +- Added 5 tests: truncated binary, module without send import, 10 actors on 4 threads, i32::MIN alloc, two watchers on same target +- No new bugs found +- All 87 tests pass + +## Cycle 18 — Div-by-Zero, Extra Exports, Zero-Addr, Operation Sequence Fuzz +- Added 4 tests: division by zero trap, extra exports tolerated, zero-address send, operation sequence property fuzz +- No new bugs found +- All 91 tests pass + +## Cycle 19 — Integer Overflow Wrapping, Multi-Msg Per Tick, Hot-Swap +- Added 3 tests: i32 overflow wrapping, 5 messages in one tick, stop echo + spawn double hot-swap +- No new bugs found +- All 94 tests pass + +## Cycle 20 — memory.copy, 50-Actor Stress, Payload Integrity, Lifecycle Fuzz +- Added 4 tests: memory.copy bulk ops, 50 actors from same engine, pattern integrity check, lifecycle fuzz with random stopping +- No new bugs found +- All 98 tests pass (8 property tests) + +## Cycle 21 — OOB call_indirect, All-Guest Integration (100 Tests) +- Added 2 tests: OOB table index trap, comprehensive all-guest-module integration +- No new bugs found +- **100 tests pass milestone** — 3 bugs found and fixed total, 8 property tests, 2 flaky MT tests fixed + +## Cycle 22 — OOB memory.fill, Inline Spawn+Send, Sequential Build +- Added 3 tests: OOB memory.fill trap, native spawns WASM + sends in same handler, 10 sequential build-use-stop cycles +- All 103 tests pass + +## Cycle 23 — Conditional Send, Multi-Page, 500-Message Load +- Added 3 tests: conditional send based on payload content, 4-page initial memory, 500-message sustained load with integrity check +- All 106 tests pass + +## Cycle 24 — Mass Spawn/Stop, Echo-to-Stopping, Trait Checks +- Added 4 tests: mass spawn/stop of 100 actors, echo to stopping actor, SharedEngine Send+Sync check, WasmActor Send check +- All 110 tests pass + +## Cycle 25 — XOR Transform, Stop-Respawn, Sequential Shutdown +- Added 3 tests: in-place XOR byte transform, stop-respawn 5 rounds, sequential WASM actor shutdown +- All 113 tests pass + +## Cycle 26 — Ptr/Len Verification, Double with Empty Payload +- Added 3 tests: guest receives correct len parameter, correct ptr parameter, double with 0-byte payload +- All 116 tests pass + +## Cycle 27 — Mixed Outbox, Drop-Oldest, Full Inbox +- Added 3 tests: mixed outbox partial delivery, DropOldest mailbox policy, echo to full inbox +- All 119 tests pass + +## Cycle 28 — Budget-Bounded Echoes, Wrong Signatures, Overlapping Send +- Added 4 tests: budget limits per-tick processing, alloc wrong signature rejected, handle wrong return rejected, overlapping dest+payload +- All 123 tests pass + +## Cycle 29 — Unexported Memory, Multi-Value, Send Boundary +- Added 4 tests: memory not exported, multi-value rejected, send dest at exact boundary, send dest 1 past boundary +- All 127 tests pass + +## Cycle 30 — Reftype Rejection, Trap Isolation, Negative Alloc, Global Counter +- Added 4 tests: externref rejected, trap doesn't affect sibling, negative alloc drops, global counter persists +- All 131 tests pass + +## Cycle 31 — Zero-Length Send, Separate Engines, 50-Round Stress, Start Function +- Added 4 tests: zero-length payload delivers, two engines coexist, 50 spawn-send-stop rounds, start function init +- All 135 tests pass + +## Cycle 32 — Random Payload Fuzz, Multi-Page Data, Conditional Fan-Out, Advancing Alloc +- Added 4 tests (1 property): random payloads never panic, data segments across 3 pages, command-byte dispatch, proper bump allocator +- All 139 tests pass + +## Cycle 33 — Outbox Snapshot, Grow-Per-Alloc, Trap-After-Send, Offset-Zero Send +- Added 4 tests: outbox snapshots at send time, memory.grow per alloc, trap clears valid outbox entries, offset 0 valid +- All 143 tests pass + +## Cycle 34 — From, 2-Thread 3-Actor MT, If/Else Branching +- Added 3 tests: From conversion, 3 WASM actors on 2 threads, if/else dispatch +- All 146 tests pass + +## Cycle 35 — Loop Sum, dest_ptr=0, Stop-With-Pending, Truncated WASM Fuzz +- Added 4 tests (1 property): loop-based byte sum, dest_ptr=0 valid, stop with pending msgs, truncated WASM never panics +- All 150 tests pass + +## Cycle 36 — FIFO Ordering, Payload Overflow, 100-Send Burst, br_table +- Added 4 tests: message FIFO order, payload ptr overflow traps, 100 sends in one handle, br_table dispatch +- Fixed prop_truncated_wasm test (renamed, relaxed assertion) +- All 154 tests pass + +## Cycle 37 — Mutual Watch, Select Instr, WASM-WASM-Native Relay, Dest Addr Fuzz +- Added 4 tests (1 property): watcher notified on stop, select instruction, 3-layer relay, arbitrary dest address +- All 158 tests pass + +## Cycle 38 — Native Spawns WASM, Many Locals, 1000 Ticks, Send-To-Dead +- Added 4 tests: native handler spawns WASM, 4-local arithmetic, 1000+ tick survival, send to dead actor +- All 162 tests pass + +## Cycle 39 — Static Alloc Overwrite, Nested Blocks, memory.size, Spawn-Stop Fuzz +- Added 4 tests (1 property): same ptr overwrite, nested block/br, memory.size instruction, lifecycle fuzz +- All 166 tests pass + +## Campaign Summary (ongoing) +- **44 commits** on bin-runner branch (3 bug fixes + 41 test commits) +- **166 tests** (151 scenario + 12 property + 3 compile-time checks) +- **3 bugs found and fixed** in core WASM actor code and runtime +- **2 flaky MT tests fixed** with retry polling +- **~6,100 lines** of test code +- Implementation proved extremely robust after initial 3 bug fixes diff --git a/crates/wasm-actor/tests/wasm_actor.rs b/crates/wasm-actor/tests/wasm_actor.rs index 1137d92..d63ad92 100644 --- a/crates/wasm-actor/tests/wasm_actor.rs +++ b/crates/wasm-actor/tests/wasm_actor.rs @@ -6124,3 +6124,139 @@ proptest! { while let Some(_) = inbox.try_recv() {} } } + +// ── Guest modifies memory between alloc and handle being called ───────────── + +#[test] +fn guest_start_function_modifies_alloc_region() { + // Guest's start function writes data in the alloc region (4096+). + // When handle is called, the host writes over it. Tests that + // the host always writes fresh data, not relying on zeroed memory. + let wat = r#" + (module + (import "swactor" "send" (func $send (param i32 i32 i32))) + (memory (export "memory") 1) + ;; Pre-fill region at 4096 with 0xFF bytes via data segment + (data (i32.const 4096) "\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff\ff") + + (func (export "alloc") (param i32) (result i32) i32.const 4096) + (func (export "handle") (param $ptr i32) (param $len i32) + ;; Echo payload to dest — host wrote message at 4096, overwriting 0xFF + (call $send + (local.get $ptr) + (i32.add (local.get $ptr) (i32.const 32)) + (i32.sub (local.get $len) (i32.const 32)) + ) + ) + ) + "#; + 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"overwrite")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().expect("host should overwrite pre-filled memory"); + assert_eq!(msg.0, b"overwrite", "host should write fresh data over 0xFF"); +} + +// ── Engine clone shares same underlying engine ────────────────────────────── + +#[test] +fn engine_clone_is_same_engine() { + let engine1 = SharedEngine::new().unwrap(); + let engine2 = engine1.clone(); + + // Both should produce working actors + let actor1 = WasmActorBuilder::new(engine1, guest_wasm("echo")).build().unwrap(); + let actor2 = WasmActorBuilder::new(engine2, guest_wasm("echo")).build().unwrap(); + + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + let a1 = rt.spawn(actor1).unwrap(); + let a2 = rt.spawn(actor2).unwrap(); + + rt.send_to(a1, framed_msg(inbox.addr(), b"clone1")).unwrap(); + rt.send_to(a2, framed_msg(inbox.addr(), b"clone2")).unwrap(); + rt.tick(); + + let mut msgs: Vec> = Vec::new(); + while let Some(msg) = inbox.try_recv() { + msgs.push(msg.0); + } + msgs.sort(); + assert_eq!(msgs, vec![b"clone1".to_vec(), b"clone2".to_vec()]); +} + +// ── Guest with i64 operations in handle ───────────────────────────────────── + +#[test] +fn handle_uses_i64_operations() { + // Guest performs i64 arithmetic and stores result as i32. + 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) + ;; Compute 1000000000 * 3 = 3000000000 (fits in i64 but not i32) + ;; Wrap to i32: 3000000000 mod 2^32 = 3000000000 (fits as u32) + ;; As i32: -1294967296 + ;; Store low byte: 3000000000 & 0xFF = 0x00 + ;; Actually let's just do something simpler: 100 + 200 = 300 → wrap i32 + (i32.store8 (i32.const 200) + (i32.wrap_i64 + (i64.add (i64.const 100) (i64.const 155)) + ) + ) + (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(); + + rt.send_to(addr, framed_msg(inbox.addr(), b"i64")).unwrap(); + rt.tick(); + + let msg = inbox.try_recv().unwrap(); + assert_eq!(msg.0[0], 255u8, "100 + 155 = 255"); +} + +// ── 200 actors from same engine all process one message ───────────────────── + +#[test] +fn two_hundred_actors_from_same_engine() { + let engine = SharedEngine::new().unwrap(); + let rt = Runtime::new(RuntimeConfig::default()); + let inbox = rt.new_inbox::().unwrap(); + + let mut addrs = Vec::new(); + for _ in 0..200 { + let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("echo")) + .build() + .unwrap(); + addrs.push(rt.spawn(actor).unwrap()); + } + + for (i, addr) in addrs.iter().enumerate() { + rt.send_to(*addr, framed_msg(inbox.addr(), &[(i & 0xFF) as u8])).unwrap(); + } + rt.tick(); + + let mut count = 0; + while let Some(_) = inbox.try_recv() { + count += 1; + } + assert_eq!(count, 200, "all 200 actors should echo"); +}