spikes/wasm-phase4 — production mechanics kill-test spike
Phase 4 validates the operational mechanics needed before a WASM worker can be
trusted in production. This spike is intentionally smaller than the future UCE
worker integration: it uses tiny WAT modules instead of generated UCE units, but
it exercises the Wasmtime controls and workspace cleanup paths the worker will
use. It is also deliberately the first spike on the Wasmtime-specific C++ API
(wasmtime.hh) — fuel, epochs, store limiters, and snapshots do not exist in
the portable wasm.h surface, so runtime-agnosticism ends here by design.
Run on k-uce:
bash spikes/wasm-phase4/build_runner.sh
/tmp/uce/wasm-phase4/runner
Expected final line:
PHASE4 EXIT CRITERION: PASS
What this proves:
- Reusable compiled artifact / snapshot proxy: modules are compiled once and then instantiated in fresh stores. Not OS CoW yet, but it validates the shape of "shared core artifact + per-request workspace birth".
- Unharmed worker: after all six kill cases, the same engine serves a healthy request that completes with the expected value. This — not merely "didn't crash" — is the worker half of the Phase 4 exit criterion.
- CPU limits, both mechanisms: fuel (deterministic, per-instruction cost)
and epoch interruption (near-zero overhead, ticker thread, traps as
interrupt). Production default is epoch per the Phase 0 findings; fuel is validated as the deterministic fallback. Note: the engine epoch only advances, so every store must set its own deadline. - Memory limit, actually load-bearing: the OOM fixture grows by 10 pages —
within its declared max (100) — so only the store limiter (2 pages) can deny
the growth. The denial is observed by the guest (
memory.grow→ -1) and converted to a trap. - Trap-to-error-page data path: every kill case's gate asserts the captured
message contains a
wasm backtraceand the expected cause. Fixture frames show<unknown>— readable production traces require units to keep their name section (or a symbolication side-file in the artifact cache). - Trace summarizer (
src/lib/wasm_trace.h): trap messages are rendered through the production collapse facility (repeated frames →×Nlines, mangled symbols demangled, cause/detail split out). The stack-exhaustion case gates it on live trap output here;site/tests/core.ucegates the parsing on canned messages in the native suite. - Handle cleanup with falsifiable checks: closers must run exactly once per handle and while the store is still alive (production closers may flush guest-resident state); destructor-only cleanup fails the ordering check.
Kill fixtures (all genuinely trap):
unreachable:__builtin_trapanalog.oob-access: wild pointer; load at 128 KiB from a 64 KiB memory.stack-exhaustion: runaway recursion →call stack exhausted.infinite-loop-fuel/infinite-loop-epoch: same loop, both CPU limits.oom-limiter: limiter-deniedmemory.grow→ guest converts -1 to a trap.
A literal C++ null-pointer dereference is deliberately absent: address 0 is
valid wasm linear memory, so *(int*)nullptr does not trap — it silently
writes inside the workspace, which is then dropped at request end (still
strictly better than a native SIGSEGV). See WASM-PROPOSAL §10 for the recorded
risk/policy.
Still deferred to production Phase 4:
- real core snapshot + OS-level CoW birth;
- wiring
wasm_trace.hsummaries into the UCE error-page UI (the summarizer itself is done and gated); name-section policy for unit artifacts; - wiring these traps into
linux_fastcgi.cpp/ the WASM worker backend; - real runtime handle-table cleanup for sqlite/mysql/sockets/tasks;
- artifact ABI versioning end-to-end;
- memory-limit policy for guest allocators that return failure instead of trapping (bump allocator vs dlmalloc behavior under the limiter).