Repository navigation
[go-fan] Go Module Review: wazero (tetratelabs/wazero) #4643
Description
🐹 Go Fan Report: wazero
Module Overview
wazero is a zero-dependency, pure-Go WebAssembly runtime. It supports WASI preview1, JIT compilation, custom host functions, and context-cancellation-aware WASM execution. It's used in this project to run DIFC security guard policies as sandboxed WASM binaries at the MCP gateway boundary.
Version in use: v1.11.0
Current Usage in gh-aw-mcpg
The entire wazero usage lives in internal/guard/wasm.go and its test.
- Files: 2 (
wasm.go,wasm_test.go) - Imports:
wazero,wazero/api,wazero/imports/wasi_snapshot_preview1,wazero/sys - Key APIs Used:
wazero.NewRuntimeConfigCompiler()+WithCloseOnContextDone(true)— JIT runtime with context supportwazero.NewCompilationCache()— process-level compilation cache shared across all guard instanceswasi_snapshot_preview1.Instantiate()— WASI layer for guest modulesruntime.NewHostModuleBuilder("env")— custom host module exposingcall_backendandhost_logto WASMmodule.ExportedFunction()/fn.Call()— invoking guard functions (label_agent,label_resource,label_response)module.Memory().Read()/.Write()/.Grow()— data exchange across the host/WASM boundarysys.ExitError— distinguishing clean WASI exits from execution trapscontext.WithoutCancel()— non-cancelable context for dealloc cleanup
The code is generally well-written and uses wazero idiomatically. Notable strengths:
- Guard traps permanently poison the guard instance (safe failure mode)
- Adaptive output buffer (4MB → 16MB, 3 retries) handles large responses
- Preferred allocator path (
alloc/deallocexports) avoids corrupting the WASM heap - stdin is explicitly isolated to prevent guards from reading the MCP protocol stream
Research Findings
Recent Updates
wazero v1.11.x continues a mature, stable API. The api.Memory interface has offered typed read/write helpers (ReadUint32Le, WriteUint32Le, etc.) since v1.0. The current usage occasionally manually decodes integers that these helpers could handle directly.
Best Practices (from wazero maintainers)
- Use
ReadUint32Le/WriteUint32Lefor integer memory I/O instead of manual bit-shifting - Close
CompilationCacheon process shutdown to release JIT resources - Export
alloc/deallocfrom guards for safe buffer management (project already prefers this path)
Improvement Opportunities
🏃 Quick Wins
1. Use api.Memory.ReadUint32Le() instead of manual bit-shifting
In tryCallWasmFunction, the required buffer size is decoded from WASM memory twice (allocator path and fallback path) using manual bit operations:
// Current — manual LE decode in two places
requiredSize := uint32(sizeBytes[0]) | uint32(sizeBytes[1])<<8 |
uint32(sizeBytes[2])<<16 | uint32(sizeBytes[3])<<24wazero's api.Memory already provides a typed helper:
// Cleaner with wazero's built-in helper
requiredSize, ok := mem.ReadUint32Le(outputPtr)
if ok && requiredSize > 0 {
return nil, requiredSize, nil
}This eliminates the intermediate sizeBytes slice read and the manual arithmetic, and is self-documenting. Applies to the duplicated pattern at both the allocator and fallback code paths.
2. Route WASM warn/error logs to the operational file logger
hostLog currently dispatches all log levels (debug/info/warn/error) only to the debug logger (logWasm):
case logLevelWarn:
logWasm.Printf("%sWARN: %s", prefix, msg)
case logLevelError:
logWasm.Printf("%sERROR: %s", prefix, msg)WASM guard errors and warnings are important operational signals that should also appear in mcp-gateway.log and gateway.md. The fix is to additionally call the operational logger for warn/error:
case logLevelWarn:
logWasm.Printf("%sWARN: %s", prefix, msg)
logger.LogWarn("guard", "[%s] %s", g.name, msg)
case logLevelError:
logWasm.Printf("%sERROR: %s", prefix, msg)
logger.LogError("guard", "[%s] %s", g.name, msg)✨ Feature Opportunities
3. Close the global compilation cache on graceful shutdown
globalCompilationCache is initialized at package startup but never closed in production. Only wasm_test.go's TestMain properly closes it. During server shutdown, Registry.Close() closes individual guard runtimes, but the shared cache is leaked.
A minimal fix is to expose a package-level closer and call it during shutdown:
// In wasm.go
func CloseGlobalCompilationCache(ctx context.Context) error {
return globalCompilationCache.Close(ctx)
}// In server/unified.go shutdown path, after guardRegistry.Close():
if err := guard.CloseGlobalCompilationCache(ctx); err != nil {
logger.LogWarn("shutdown", "Failed to close WASM compilation cache: %v", err)
}This ensures JIT-compiled code is properly released on graceful shutdown, which matters especially for long-running gateway processes.
4. Consider interpreter fallback for restricted environments
The code unconditionally uses NewRuntimeConfigCompiler() (JIT). In hardened container environments with mmap/mprotect restrictions, JIT initialization may fail at startup. A graceful fallback to NewRuntimeConfigInterpreter() would improve robustness in such deployments. A WasmGuardOptions.UseInterpreter bool field could opt into the interpreter for testing or restricted environments.
📐 Best Practice Alignment
5. Fallback memory layout risks heap corruption
When guards don't export alloc/dealloc, the code grows WASM memory and places host-managed buffers at the very end of linear memory. If the WASM module's heap allocator has already grown into that region, this will silently overwrite guard-owned memory.
The safest fix is to require alloc/dealloc exports, returning a clear error for guards that don't provide them. The existing error message for missing guard functions provides a good model:
if allocFn == nil {
return nil, fmt.Errorf("WASM module must export alloc and dealloc for safe memory management. " +
"See examples/guards/sample-guard/README.md for details")
}Alternatively, document this limitation prominently in the guard authoring guide.
6. isWasmTrap string matching is fragile
The fallback in isWasmTrap uses:
return strings.Contains(err.Error(), "wasm error:")This will silently break if wazero changes its error message format. Consider opening an upstream issue requesting a typed api.TrapError or similar that can be detected with errors.As. For now, adding a comment noting the wazero version this was verified against would help future maintainers.
Recommendations (Prioritized)
- [Medium] Expose
CloseGlobalCompilationCacheand wire it into graceful shutdown — prevents resource leaks in production (Lpcox/initial implementation #3 above) - [Low] Route WASM warn/error log levels to the operational file logger for better observability (Lpcox/initial implementation #2 above)
- [Low] Replace manual LE uint32 decode with
mem.ReadUint32Le()— cleaner code (Configure as a Go CLI tool #1 above) - [Future] Require
alloc/deallocexports or document heap-overlap risk in guard authoring guide (Updated Dockerfile #5 above) - [Future] Consider interpreter fallback option for restricted environments (Lpcox/add difc #4 above)
Next Steps
- Add
CloseGlobalCompilationCache(ctx)tointernal/guard/wasm.goand call it inserver/unified.goshutdown - Update
hostLogto route warn/error tologger.LogWarn/logger.LogError - Replace manual
uint32LE decoding withmem.ReadUint32Le() - Add guidance to guard authoring docs about
alloc/deallocexports being required for safe memory management
Generated by Go Fan 🐹
Module summary saved to: specs/mods/wazero.md (session artifact)
Run: §24983047267
Note
🔒 Integrity filter blocked 8 items
The following items were blocked because they don't meet the GitHub integrity level.
- open-telemetry/opentelemetry-go@7e91b17
get_commit: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - santhosh-tekuri/jsonschema@d3bf053
get_commit: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - wazero/wazero@bfb20e0
get_commit: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0
get_latest_release: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - https://github.com/santhosh-tekuri/jsonschema/releases/tag/v6.0.2
get_latest_release: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - https://github.com/wazero/wazero/releases/tag/v1.11.0
get_latest_release: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - get_file_contents
get_file_contents: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved". - search_code
search_code: has lower integrity than agent requires. The agent cannot read data with integrity below "unapproved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by Go Fan · ● 2.7M · ◷
- expires on May 4, 2026, 7:58 AM UTC
Activity