test(wasm-actor): cycle 63 — zero-len send, overlapping regions, alloc exhaustion (274 tests)

Added 4 tests: zero-length payload from guest, overlapping dest/payload regions,
alloc exhaustion drops gracefully, stop all actors in runtime. No new bugs found.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
Claude 2026-02-13 10:18:25 +00:00
parent c304ad5960
commit 8d020bbf1e

View file

@ -9224,3 +9224,144 @@ fn thirty_two_kb_payload_echo() {
assert_eq!(resp.0.len(), 32768); assert_eq!(resp.0.len(), 32768);
assert_eq!(resp.0, payload); assert_eq!(resp.0, payload);
} }
// ── Cycle 63 ─────────────────────────────────────────────────────────────────
// Guest sends with payload_len=0 but valid payload_ptr — zero-length payload delivered
#[test]
fn send_zero_length_payload_from_guest() {
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)
;; Send with payload_len=0 — should deliver empty payload
(call $send (local.get $ptr) (i32.const 0) (i32.const 0))
)
)"#;
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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Frame message: first 32 bytes are dest (inbox addr)
let mut msg = Vec::with_capacity(32);
msg.extend_from_slice(&inbox.addr().0);
rt.send_to(addr, ByteMessage(msg)).unwrap();
rt.tick();
let resp = inbox.try_recv().expect("zero-length payload should arrive");
assert!(resp.0.is_empty(), "payload should be empty");
}
// Guest sends with overlapping dest and payload regions (payload starts inside dest)
#[test]
fn send_with_overlapping_dest_and_payload_regions() {
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)
;; dest_ptr=1024, payload_ptr=1040 (inside the 32-byte dest region), payload_len=8
;; This overlaps: dest is [1024..1056], payload is [1040..1048]
(call $send (i32.const 1024) (i32.const 1040) (i32.const 8))
)
)"#;
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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Write the inbox address into memory at offset 1024 (via the message)
let mut msg = Vec::with_capacity(64);
msg.extend_from_slice(&inbox.addr().0);
msg.extend_from_slice(&[0u8; 32]); // padding
rt.send_to(addr, ByteMessage(msg)).unwrap();
rt.tick();
// The send should succeed — overlapping reads are fine (read-only)
let resp = inbox.try_recv().expect("overlapping regions should work");
assert_eq!(resp.0.len(), 8);
}
// Sequential alloc exhaustion: echo actor with tiny heap eventually can't alloc
#[test]
fn alloc_exhaustion_drops_message_gracefully() {
let wat = r#"(module
(import "swactor" "send" (func $send (param i32 i32 i32)))
(memory (export "memory") 1)
;; Tiny heap: starts at 60000, only 5536 bytes before page end
(global $heap (mut i32) (i32.const 60000))
(func (export "alloc") (param $len i32) (result i32)
(local $ptr i32)
(local.set $ptr (global.get $heap))
;; Check if allocation would exceed page
(if (i32.gt_u
(i32.add (global.get $heap) (local.get $len))
(i32.const 65536))
(then (return (i32.const -1))) ;; OOM signal
)
(global.set $heap (i32.add (global.get $heap) (local.get $len)))
(local.get $ptr)
)
(func (export "handle") (param $ptr i32) (param $len i32)
;; Echo: send first 32 bytes as dest, rest as payload
(if (i32.ge_u (local.get $len) (i32.const 33))
(then
(call $send
(local.get $ptr)
(i32.add (local.get $ptr) (i32.const 32))
(i32.sub (local.get $len) (i32.const 32))
)
)
)
)
)"#;
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::<ByteMessage>().unwrap();
let addr = rt.spawn(actor).unwrap();
// Send messages that consume heap space
for i in 0..100 {
rt.send_to(addr, framed_msg(inbox.addr(), &vec![i as u8; 100])).unwrap();
}
rt.tick();
// Count how many were echoed (some should be dropped due to OOM)
let delivered = std::iter::from_fn(|| inbox.try_recv()).count();
assert!(delivered < 100, "some messages should be dropped due to OOM (got {delivered})");
assert!(delivered > 0, "at least some messages should succeed");
}
// Stop all actors in a runtime — runtime should be empty
#[test]
fn stop_all_wasm_actors_in_runtime() {
let engine = SharedEngine::new().unwrap();
let rt = Runtime::new(RuntimeConfig::default());
let mut addrs = Vec::new();
for _ in 0..5 {
let actor = WasmActorBuilder::new(engine.clone(), guest_wasm("silent"))
.build().unwrap();
addrs.push(rt.spawn(actor).unwrap());
}
rt.tick();
for addr in &addrs {
rt.stop_actor(*addr);
}
rt.tick();
rt.tick();
// All actors stopped — sending should fail or be silently dropped
for addr in &addrs {
let _ = rt.send_to(*addr, ByteMessage(vec![]));
}
rt.tick(); // no panics
}