-
Notifications
You must be signed in to change notification settings - Fork 15
Description
πΉ Go Fan Report: tetratelabs/wazero
Module Overview
[wazero]((wazero.io/redacted) is a zero-dependency WebAssembly runtime for Go β no CGO, no shared libraries, runs on all Go-supported platforms. gh-aw-mcpg embeds wazero to execute sandboxed WASM guard plugins that implement the DIFC (Data Information Flow Control) labeling protocol. Guards are compiled to WASM (currently with TinyGo) and run in-process inside the gateway.
Version in use: v1.11.0 (current latest β)
Current Usage in gh-aw
- Files: 1 core implementation (
internal/guard/wasm.go, 987 lines), 2 call sites ininternal/server/unified.go - Key APIs Used:
wazero.NewRuntime(ctx)β runtime creation with default configwazero.NewModuleConfig().WithName("guard").WithStartFunctions()β module configurationruntime.InstantiateWithConfig(ctx, wasmBytes, moduleConfig)β compile + instantiate in one callruntime.NewHostModuleBuilder("env")β exposescall_backendandhost_logto guardswasi_snapshot_preview1.Instantiate(ctx, runtime)β WASI system call layerapi.Module,api.Function,api.Memory,api.GoModuleFuncβ low-level module interactionmem.Grow(pages)/mem.Read()/mem.Write()β manual WASM linear memory management
Pattern: Each guard gets its own wazero.Runtime (1:1 mapping). A mutex serializes all calls since WASM is single-threaded. An adaptive buffer retry loop (4MB β 16MB max) handles variable-size guard outputs.
Research Findings
Recent Updates (v1.11.0, ~2026-03-01)
- π First external dependency added:
golang.org/x/sys(see RATIONALE.md Β§why-xsys) - π§ Requires Go 1.24+ β project uses Go 1.25.0 β
- π Extended const expressions: New WebAssembly feature support in the compiler backend (wazevo)
- Latest commit (2026-03-08): Full implementation of extended const expressions
Best Practices from wazero Maintainers
- Two-phase lifecycle: Prefer
CompileModule+InstantiateModuleover the combinedInstantiateWithConfigto separate compilation from instantiation - Context propagation: Use
RuntimeConfig.WithCloseOnContextDone(true)with per-request contexts to interrupt stuck execution - Module isolation: Set explicit stdin/stdout/stderr on
ModuleConfigrather than inheriting host handles - Memory limits: Use
ModuleConfig.WithMemoryLimitPages()to cap per-module memory
Improvement Opportunities
π Quick Wins
1. Isolate stdin from host process
NewModuleConfig() currently inherits the host's stdin:
// Current: no stdin isolation
moduleConfig := wazero.NewModuleConfig().WithName("guard").WithStartFunctions()
// Fix: prevent WASM from reading gateway's stdio transport
moduleConfig := wazero.NewModuleConfig().
WithName("guard").
WithStartFunctions().
WithStdin(strings.NewReader("")) // Isolate stdinThe gateway uses stdin for MCP protocol communication. A misbehaving WASM guard could accidentally consume bytes from stdin, corrupting the MCP session.
2. Explicit runtime compiler config
// Current: implicit default
runtime := wazero.NewRuntime(ctx)
// Better: explicit JIT intent
runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())Self-documents the performance intent and makes it obvious if the interpreter fallback is ever needed (e.g., wazero.NewRuntimeConfigInterpreter() for environments without mmap).
β¨ Feature Opportunities
3. Context-propagated guard timeouts via WithCloseOnContextDone
Currently, a guard call uses g.ctx (the gateway startup context), not the per-request context. This means a hung WASM guard blocks the gateway indefinitely. wazero supports interrupting execution via context cancellation:
// RuntimeConfig with context-based interrupt
cfg := wazero.NewRuntimeConfigCompiler().WithCloseOnContextDone(true)
runtime := wazero.NewRuntimeWithConfig(ctx, cfg)This pairs with fixing the context-in-struct anti-pattern (see Β§Best Practice #4 below) to allow request-scoped timeouts to propagate into guard execution.
4. Separation of compile and instantiate phases
The current pattern compiles the WASM binary on every NewWasmGuard call:
// Current: compile + instantiate in one step, no reuse possible
module, err := runtime.InstantiateWithConfig(ctx, wasmBytes, moduleConfig)The wazero two-phase pattern:
// Phase 1: compile (can be cached/reused)
compiled, err := runtime.CompileModule(ctx, wasmBytes)
if err != nil { ... }
defer compiled.Close(ctx)
// Phase 2: instantiate (cheap after compilation)
module, err := runtime.InstantiateModule(ctx, compiled, moduleConfig)This enables future hot-reload: re-instantiate from a cached CompiledModule without re-parsing/re-compiling the WASM binary.
π Best Practice Alignment
5. Context stored in struct (g.ctx context.Context)
Go's guidelines explicitly state: "Do not store Contexts inside a struct type." The current WasmGuard stores the initialization context and reuses it for every WASM call:
// Current: stores startup context, breaks request-scoped cancellation
type WasmGuard struct {
ctx context.Context // β anti-pattern
...
}The request context passed to LabelAgent, LabelResource, and LabelResponse is currently only used for GetRequestStateFromContext but NOT for the actual WASM execution. Propagating ctx through callWasmFunction β tryCallWasmFunction β wasmAlloc/wasmDealloc would:
- Fix the anti-pattern
- Enable
WithCloseOnContextDonetimeouts to work correctly - Allow request cancellations to propagate into guard execution
6. Memory limit for guard isolation
No cap on WASM linear memory growth. A guard that loops calling mem.Grow() could exhaust gateway memory. ModuleConfig.WithMemoryLimitPages(maxPages) (or an equivalent runtime-level limit) would bound each guard's footprint.
π§ General Improvements
7. Missing unit tests for WASM runtime
internal/guard/wasm.go (987 lines) has no wasm_test.go. The guard_test.go file doesn't exercise the WASM execution path at all. Key things to test:
- Buffer retry logic (4MB β 8MB β 16MB)
alloc/deallocvs host-managed memory paths- Error handling for malformed WASM
- Guard function validation (missing exports)
A minimal WAT (WebAssembly Text Format) fixture could serve as a zero-dependency test binary without requiring TinyGo.
8. Richer call_backend error codes
call_backend returns a single sentinel ^uint32(0) (0xFFFFFFFF) for all errors. Guards cannot distinguish between timeout, serialization failure, or backend unavailability. A richer error code scheme would allow guards to implement retry or fallback logic.
Recommendations (Prioritized)
| Priority | Item | Effort | Impact |
|---|---|---|---|
| π΄ High | Fix context-in-struct (g.ctx) β enables timeouts |
Medium | Security/Reliability |
| π΄ High | Add .WithStdin(strings.NewReader("")) |
Trivial | Security |
| π‘ Medium | Add WithCloseOnContextDone(true) + pass ctx |
Medium | Reliability |
| π‘ Medium | Add unit tests (wasm_test.go with WAT fixtures) |
Medium | Correctness |
| π’ Low | Explicit NewRuntimeConfigCompiler() |
Trivial | Clarity |
| π’ Low | Split compile/instantiate phases | Small | Futureproofing |
| π’ Low | Memory limit via WithMemoryLimitPages |
Small | Resource safety |
Next Steps
- Fix
g.ctx context.Contextstruct field β pass request ctx through WASM call chain - Add
.WithStdin(strings.NewReader(""))toNewModuleConfig() - Add
WithCloseOnContextDone(true)to runtime config once ctx is propagated - Create
wasm_test.gowith WAT-based minimal WASM fixtures
Generated by Go Fan πΉ Β· Run Β§22842843797
Module summary saved to: specs/mods/wazero.md
References:
Generated by Go Fan
- expires on Mar 16, 2026, 7:41 AM UTC