test: stale outbox leak on trap — failing test (#[ignore])
When a guest calls swactor.send then traps, the outbox entry survives and leaks into the next successful handle() call. The outbox should be cleared when handle traps, since the guest's operation was incomplete. Also adds: invalid WASM bytes test, zero-length payload send test, multiple sequential traps test (all passing). Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
e7efab4157
commit
148edb9ac5
1 changed files with 185 additions and 0 deletions
|
|
@ -740,6 +740,191 @@ fn native_actor_communicates_with_wasm_actor() {
|
||||||
assert_eq!(received.0, b"from native");
|
assert_eq!(received.0, b"from native");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Stale outbox: sends before trap leak into next handle ─────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore] // BUG: outbox not cleared on handle trap — stale entries leak into next call
|
||||||
|
fn outbox_entries_from_trapped_handle_do_not_leak_into_next_call() {
|
||||||
|
// A guest that calls swactor.send() successfully, then traps.
|
||||||
|
// The outbox contains the send from before the trap.
|
||||||
|
// On the next handle call (which succeeds without sending), the stale
|
||||||
|
// outbox entry should NOT be delivered.
|
||||||
|
//
|
||||||
|
// Counter incremented BEFORE the if-branch so it persists past the trap.
|
||||||
|
let wat = r#"
|
||||||
|
(module
|
||||||
|
(import "swactor" "send" (func $send (param i32 i32 i32)))
|
||||||
|
(memory (export "memory") 1)
|
||||||
|
(global $counter (mut i32) (i32.const 0))
|
||||||
|
|
||||||
|
(func (export "alloc") (param i32) (result i32)
|
||||||
|
i32.const 256
|
||||||
|
)
|
||||||
|
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||||
|
;; Increment counter first (survives trap)
|
||||||
|
global.get $counter
|
||||||
|
i32.const 1
|
||||||
|
i32.add
|
||||||
|
global.set $counter
|
||||||
|
|
||||||
|
;; If counter was 0 (now 1): send then trap
|
||||||
|
global.get $counter
|
||||||
|
i32.const 1
|
||||||
|
i32.eq
|
||||||
|
if
|
||||||
|
local.get $ptr ;; dest_ptr (first 32 bytes = inbox address)
|
||||||
|
i32.const 32 ;; payload_ptr
|
||||||
|
i32.const 1 ;; payload_len
|
||||||
|
call $send
|
||||||
|
unreachable ;; trap after send
|
||||||
|
end
|
||||||
|
;; counter > 1: do nothing (no send)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
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::<ByteMessage>().unwrap();
|
||||||
|
let addr = rt.spawn(actor).unwrap();
|
||||||
|
|
||||||
|
// First message: guest sends to outbox then traps — stale entry in outbox
|
||||||
|
// Use framed_msg so the first 32 bytes are the inbox address
|
||||||
|
rt.send_to(addr, framed_msg(inbox.addr(), b"x")).unwrap();
|
||||||
|
rt.tick();
|
||||||
|
|
||||||
|
// No message should have been delivered (handle trapped before outbox drain)
|
||||||
|
assert!(inbox.try_recv().is_none(), "trapped handle should not deliver messages");
|
||||||
|
|
||||||
|
// Second message: guest does nothing (counter=2, no send, no trap).
|
||||||
|
// If the outbox wasn't cleared, the stale entry would be drained here.
|
||||||
|
rt.send_to(addr, framed_msg(inbox.addr(), b"y")).unwrap();
|
||||||
|
rt.tick();
|
||||||
|
|
||||||
|
// Should still be empty — the stale outbox entry must not leak
|
||||||
|
assert!(
|
||||||
|
inbox.try_recv().is_none(),
|
||||||
|
"stale outbox entry from trapped call should not leak into next handle"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Builder validation: invalid WASM bytes ───────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_wasm_bytes_returns_wasmtime_error() {
|
||||||
|
let garbage = vec![0u8, 1, 2, 3]; // not valid wasm
|
||||||
|
let engine = SharedEngine::new().unwrap();
|
||||||
|
let result = WasmActorBuilder::new(engine, garbage).build();
|
||||||
|
match result {
|
||||||
|
Err(WasmActorError::Wasmtime(_)) => {} // expected — compilation failure
|
||||||
|
Err(other) => panic!("expected Wasmtime error for invalid bytes, got: {other}"),
|
||||||
|
Ok(_) => panic!("should reject invalid wasm bytes"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Guest sends zero-length payload ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guest_send_with_zero_length_payload_delivers_empty_message() {
|
||||||
|
// Guest calls swactor.send with payload_len=0. This should produce
|
||||||
|
// a ByteMessage(vec![]) at the destination.
|
||||||
|
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 ;; valid allocation
|
||||||
|
)
|
||||||
|
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||||
|
;; Send with the first 32 bytes as dest, zero-length payload
|
||||||
|
local.get $ptr
|
||||||
|
i32.const 32 ;; payload_ptr (doesn't matter, len is 0)
|
||||||
|
i32.const 0 ;; payload_len
|
||||||
|
call $send
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
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::<ByteMessage>().unwrap();
|
||||||
|
let addr = rt.spawn(actor).unwrap();
|
||||||
|
|
||||||
|
// Build a framed message with the inbox address as the first 32 bytes
|
||||||
|
let msg = framed_msg(inbox.addr(), b"ignored-payload");
|
||||||
|
rt.send_to(addr, msg).unwrap();
|
||||||
|
rt.tick();
|
||||||
|
|
||||||
|
let received = inbox.try_recv().expect("should receive zero-length message");
|
||||||
|
assert!(received.0.is_empty(), "payload should be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Multiple sequential traps: actor survives repeated failures ──────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn actor_survives_multiple_sequential_traps() {
|
||||||
|
// After 3 consecutive traps, the actor should still be alive and
|
||||||
|
// able to process a non-trapping message.
|
||||||
|
let wat = r#"
|
||||||
|
(module
|
||||||
|
(import "swactor" "send" (func $send (param i32 i32 i32)))
|
||||||
|
(memory (export "memory") 1)
|
||||||
|
(global $counter (mut i32) (i32.const 0))
|
||||||
|
(func (export "alloc") (param i32) (result i32)
|
||||||
|
i32.const 256
|
||||||
|
)
|
||||||
|
(func (export "handle") (param $ptr i32) (param $len i32)
|
||||||
|
global.get $counter
|
||||||
|
i32.const 3
|
||||||
|
i32.lt_u
|
||||||
|
if
|
||||||
|
;; First 3 calls: trap
|
||||||
|
global.get $counter
|
||||||
|
i32.const 1
|
||||||
|
i32.add
|
||||||
|
global.set $counter
|
||||||
|
unreachable
|
||||||
|
end
|
||||||
|
;; 4th+ call: echo the message back using first 32 bytes as dest
|
||||||
|
local.get $ptr
|
||||||
|
local.get $ptr
|
||||||
|
i32.const 32
|
||||||
|
i32.add
|
||||||
|
local.get $len
|
||||||
|
i32.const 32
|
||||||
|
i32.sub
|
||||||
|
call $send
|
||||||
|
)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
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::<ByteMessage>().unwrap();
|
||||||
|
let addr = rt.spawn(actor).unwrap();
|
||||||
|
|
||||||
|
// 3 trapping messages
|
||||||
|
for _ in 0..3 {
|
||||||
|
rt.send_to(addr, framed_msg(inbox.addr(), b"will trap")).unwrap();
|
||||||
|
rt.tick();
|
||||||
|
assert!(inbox.try_recv().is_none(), "trapped call should produce nothing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4th message: should succeed
|
||||||
|
let payload = b"survived";
|
||||||
|
rt.send_to(addr, framed_msg(inbox.addr(), payload)).unwrap();
|
||||||
|
rt.tick();
|
||||||
|
|
||||||
|
let received = inbox.try_recv().expect("actor should work after multiple traps");
|
||||||
|
assert_eq!(received.0, payload);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Multi-worker: WASM actors across threads ─────────────────────────────────
|
// ── Multi-worker: WASM actors across threads ─────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue