Skip to content

[go-fan] Go Module Review: tetratelabs/wazeroΒ #1696

@github-actions

Description

@github-actions

🐹 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

  1. Two-phase lifecycle: Prefer CompileModule + InstantiateModule over the combined InstantiateWithConfig to separate compilation from instantiation
  2. Context propagation: Use RuntimeConfig.WithCloseOnContextDone(true) with per-request contexts to interrupt stuck execution
  3. Module isolation: Set explicit stdin/stdout/stderr on ModuleConfig rather than inheriting host handles
  4. 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

  • Fix g.ctx context.Context struct field β€” pass request ctx through WASM call chain
  • Add .WithStdin(strings.NewReader("")) to NewModuleConfig()
  • Add WithCloseOnContextDone(true) to runtime config once ctx is propagated
  • Create wasm_test.go with 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions