Skip to content
GitHub

Two sandboxes

The Function node runs on goja (see Security posture). A call costs 11.6 to 13.7 µs. It is not a boundary against memory: a function that allocates in a loop grows the Go heap until the pod dies, and all goja can do about it is a wall-clock timeout.

So there’s a second sandbox: a WebAssembly host on wazero, in internal/wasmhost, where linear memory has a hard ceiling and a guest that allocates past it gets a trap while the host carries on unbothered. That costs 184 to 196 µs a call, fifteen to sixteen times a goja call. Worth paying exactly when you’re running code you don’t fully trust, and not otherwise.

Both figures are go test -run=XXX -bench='FunctionSimple|WasmCall' -benchmem -count=5 ./internal/nodes/ ./internal/wasmhost/, on the same box, commit and day as the cloning numbers above. The WASM benchmark needs the test guest built first, exactly as CI builds it.

Here’s the catch: no node uses the WASM host yet. It’s real, it’s tested, and CI fails if those tests quietly skip. But nothing in the palette calls it and it isn’t compiled into the binary, so today you cannot run a WASM guest in a flow. The Function node is goja and only goja. A wasm node on this host is Phase 6 of the roadmap, and until it ships, everything below describes the host, not something you can drag onto a canvas.

When it does ship, a node can be written in Rust, TinyGo, Zig or AssemblyScript instead of JavaScript, so the signal processing an OT flow actually wants runs compiled, with a memory ceiling it can’t argue with.

The guest ABI is three exports, two of them allocator hooks, and it’s that small on purpose. Every export is attack surface, and one more thing every guest author has to get right.

Export Signature Required
hotloop_flow_process (ptr i32, len i32) -> i64 yes
hotloop_flow_alloc (size i32) -> i32 yes
hotloop_flow_free (ptr i32, size i32) no

The i64 result packs an offset in the high 32 bits and a length in the low 32, pointing at a JSON response in the guest’s own memory. Everything crosses as JSON, so a guest in any language can produce it without a shared schema compiler.

The limits are small on purpose. This runs on edge hardware next to the flows that actually matter, and a node that needs more should have to say so:

Details goja WASM
Timeout per call 5 s 5 s
Memory ceiling none enforceable 64 MiB, hard
Max output 16 MiB 8 MiB

Both runtimes are pure Go with no cgo. That’s what keeps the build CGO_ENABLED=0 and the image distroless today, and keeps both true when the WASM host goes into the binary. Pull in one cgo dependency and the static binary and the distroless image go with it, and CI fails the build to tell you so.