feat: extended features and testing for the WASM runtime actor prototype (#36)

Harden the WASM actor host against malformed guest modules and add a property-test-backed suite covering builder validation, message handling, allocation, trap recovery, and native interop.

- crates/wasm-actor/src/actor.rs: bounds-check the guest memory write against the alloc-returned pointer (saturating_add plus OOB guard) so an out-of-bounds allocation drops the message instead of panicking the host
- crates/wasm-actor/src/actor.rs: clear the host outbox when the guest handle traps, discarding partial sends from the incomplete operation while keeping the actor alive
- crates/wasm-actor/tests/wasm_actor.rs: add a comprehensive suite covering builder validation (missing exports, invalid wasm, disabled features), echo round-trip, alloc trap/exhaustion recovery, send boundary conditions, full lifecycle, and wasm<->native interop/relay/watch scenarios
- crates/wasm-actor/tests/wasm_actor.rs: add proptest cases (builder fuzz, arbitrary-payload echo, arbitrary alloc returns, send-arg fuzz, lifecycle fuzz) and assert Send/Sync of the error types
- crates/wasm-actor/Cargo.toml: pull in proptest as a dev-dependency

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
zacheryasc 2026-02-13 14:11:39 +00:00
parent 18cabdb94d
commit a4a4c7ceae
4 changed files with 1266 additions and 226 deletions

1
Cargo.lock generated
View file

@ -2747,6 +2747,7 @@ dependencies = [
name = "swactor-wasm-actor"
version = "0.1.0"
dependencies = [
"proptest",
"swactor",
"wasmtime",
"wat",

View file

@ -10,3 +10,4 @@ wasmtime = "29"
[dev-dependencies]
swactor = { path = "../..", features = ["getrandom"] }
wat = "1"
proptest = "1"

View file

@ -41,12 +41,16 @@ impl ActorInterface for WasmActor {
};
// 2. Write message bytes into guest memory
self.memory.data_mut(&mut self.store)
[ptr as usize..(ptr as usize + bytes.len())]
.copy_from_slice(bytes);
let mem = self.memory.data_mut(&mut self.store);
let end = (ptr as usize).saturating_add(bytes.len());
if end > mem.len() {
return; // alloc returned OOB pointer — drop message
}
mem[ptr as usize..end].copy_from_slice(bytes);
// 3. Call guest handle
if self.handle.call(&mut self.store, (ptr, len)).is_err() {
self.store.data_mut().outbox.clear(); // discard sends from incomplete operation
return; // handle trapped — drop message, keep actor alive
}

File diff suppressed because it is too large Load diff