๐น 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 in internal/server/unified.go
- Key APIs Used:
wazero.NewRuntime(ctx) โ runtime creation with default config
wazero.NewModuleConfig().WithName("guard").WithStartFunctions() โ module configuration
runtime.InstantiateWithConfig(ctx, wasmBytes, moduleConfig) โ compile + instantiate in one call
runtime.NewHostModuleBuilder("env") โ exposes call_backend and host_log to guards
wasi_snapshot_preview1.Instantiate(ctx, runtime) โ WASI system call layer
api.Module, api.Function, api.Memory, api.GoModuleFunc โ low-level module interaction
mem.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 + InstantiateModule over the combined InstantiateWithConfig to 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
ModuleConfig rather 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 stdin
The 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
WithCloseOnContextDone timeouts 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/dealloc vs 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
Generated by Go Fan ๐น ยท Run ยง22842843797
Module summary saved to: specs/mods/wazero.md
References:
Generated by Go Fan
๐น 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
internal/guard/wasm.go, 987 lines), 2 call sites ininternal/server/unified.gowazero.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 managementPattern: 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)
golang.org/x/sys(see RATIONALE.md ยงwhy-xsys)Best Practices from wazero Maintainers
CompileModule+InstantiateModuleover the combinedInstantiateWithConfigto separate compilation from instantiationRuntimeConfig.WithCloseOnContextDone(true)with per-request contexts to interrupt stuck executionModuleConfigrather than inheriting host handlesModuleConfig.WithMemoryLimitPages()to cap per-module memoryImprovement Opportunities
๐ Quick Wins
1. Isolate stdin from host process
NewModuleConfig()currently inherits the host's stdin:The 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
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
WithCloseOnContextDoneCurrently, 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: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
NewWasmGuardcall:The wazero two-phase pattern:
This enables future hot-reload: re-instantiate from a cached
CompiledModulewithout 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
WasmGuardstores the initialization context and reuses it for every WASM call:The request context passed to
LabelAgent,LabelResource, andLabelResponseis currently only used forGetRequestStateFromContextbut NOT for the actual WASM execution. PropagatingctxthroughcallWasmFunctionโtryCallWasmFunctionโwasmAlloc/wasmDeallocwould:WithCloseOnContextDonetimeouts to work correctly6. 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 nowasm_test.go. Theguard_test.gofile doesn't exercise the WASM execution path at all. Key things to test:alloc/deallocvs host-managed memory pathsA minimal WAT (WebAssembly Text Format) fixture could serve as a zero-dependency test binary without requiring TinyGo.
8. Richer
call_backenderror codescall_backendreturns 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)
g.ctx) โ enables timeouts.WithStdin(strings.NewReader(""))WithCloseOnContextDone(true)+ pass ctxwasm_test.gowith WAT fixtures)NewRuntimeConfigCompiler()WithMemoryLimitPagesNext Steps
g.ctx context.Contextstruct field โ pass request ctx through WASM call chain.WithStdin(strings.NewReader(""))toNewModuleConfig()WithCloseOnContextDone(true)to runtime config once ctx is propagatedwasm_test.gowith WAT-based minimal WASM fixturesGenerated by Go Fan ๐น ยท Run ยง22842843797
Module summary saved to:
specs/mods/wazero.mdReferences: