Skip to content

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

@github-actions

Description

@github-actions

🐹 Go Fan Report: tetratelabs/wazero

TL;DR: wazero is used masterfully for the WASM security guard system. Three actionable improvements found: a resource-management gap in guard shutdown, fragile trap detection, and a logging namespace inconsistency.


Module Overview

github.com/tetratelabs/wazero is a zero-dependency WebAssembly runtime for Go. It provides an embeddable WASM execution engine with full WASI support, host function bridging, and JIT compilation. In this project it powers the security guard system — running Rust-compiled WASM binaries that enforce DIFC (Decentralized Information Flow Control) policies without any network access.

Version used: v1.11.0 ✅ (latest)


Current Usage in gh-aw

  • Files: 2 (internal/guard/wasm.go, internal/guard/wasm_test.go)
  • Import paths used:
    • github.com/tetratelabs/wazero — runtime, config, cache
    • github.com/tetratelabs/wazero/apiModule, Memory, Function, GoModuleFunc, ValueType*
    • github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1 — WASI instantiation
  • Key APIs utilized:
    • wazero.NewRuntimeConfigCompiler().WithCloseOnContextDone(true) (JIT mode)
    • wazero.NewCompilationCache() / NewCompilationCacheWithDir() (process-level cache)
    • runtime.NewHostModuleBuilder("env") with NewFunctionBuilder() (host functions)
    • wasi_snapshot_preview1.Instantiate() (WASI support)
    • module.ExportedFunction(), api.Module.Memory().Read()/.Write()/.Grow() (memory I/O)

Research Findings

What This Code Does Well 🏆

The wazero integration is sophisticated and shows deep understanding of the runtime:

  1. Adaptive buffer retry — Guards communicate "buffer too small" via return code -2, with the first 4 bytes optionally containing the required size. The host retries up to 3 times, growing from 4MB → 16MB.
  2. Guard-managed allocators — When a guard exports alloc/dealloc, the host uses them to avoid buffer aliasing with the guard's heap. Falls back gracefully to end-of-memory placement.
  3. Trap poisoning — A WASM trap (Rust panic → unreachable) permanently marks the guard as failed. All subsequent calls return an error immediately, preventing undefined-state execution.
  4. context.WithoutCancel for cleanup — Dealloc calls use a non-cancellable context so heap allocations aren't leaked if the request context times out during cleanup.
  5. Stdin isolationWithStdin(strings.NewReader("")) prevents WASM code from accidentally reading the gateway's MCP protocol stdin.
  6. Process-level JIT cacheglobalCompilationCache is shared across all WasmGuard instances, eliminating redundant compilation. Tests properly close it in TestMain.
  7. Proper error cleanup — Every error path in NewWasmGuardWithOptions calls runtime.Close(ctx) before returning, preventing runtime leaks during initialization failures.

Improvement Opportunities

🏃 Quick Wins

1. Guard Registry Missing Close() Method

The Registry struct has no Close() method, and WasmGuard.Close() is never called during server shutdown.

// UnifiedServer.Close() — guards NOT closed:
func (us *UnifiedServer) Close() error {
    us.launcher.Close()
    return nil  // guardRegistry.Close() never called
}

Guards are created with context.Background() (lines 49, 219 in guard_init.go), so WithCloseOnContextDone(true) is effectively disabled — the context is never cancelled.

Suggested fix: Add a Close(ctx context.Context) error method to Registry that iterates guards implementing io.Closer (or a Closer interface), and call it from UnifiedServer.Close() and InitiateShutdown().

// Registry.Close closes all registered guards that implement io.Closer.
func (r *Registry) Close(ctx context.Context) {
    r.mu.Lock()
    defer r.mu.Unlock()
    for id, g := range r.guards {
        if c, ok := g.(interface{ Close(context.Context) error }); ok {
            if err := c.Close(ctx); err != nil {
                logger.LogWarn("guard", "Failed to close guard for server %s: %v", id, err)
            }
        }
    }
}

2. Logging Namespace Confusion in registry.go

registry.go defines var debugLog = logger.New("guard:registry") for DEBUG-gated logs, but its plain log.Printf(...) calls (lines 33, 49, 79, 119) use the package-level log variable — which is declared in context.go with namespace "guard:context". This means:

  • DEBUG=guard:registry will NOT show log.Printf messages from registry.go
  • DEBUG=guard:context will show registry messages alongside unrelated context messages

Suggested fix: Replace log.Printf in registry.go with debugLog.Printf (or logger.LogInfo for operational events), and consider making those registry messages use logger.LogInfo("guard", ...) for consistent operational logging.


✨ Feature Opportunities

3. Use Typed Error Checking for WASM Traps

The current trap detection is string-based:

func isWasmTrap(err error) bool {
    return err != nil && strings.Contains(err.Error(), "wasm error:")
}

This is fragile — it depends on wazero's internal error message format remaining stable. wazero's sys package provides sys.ExitError to distinguish normal WASM process exits (e.g., TinyGo exit(0) during init) from execution traps. A normal exit with code 0 should NOT poison the guard.

Suggested fix: Use errors.As to check for sys.ExitError before the string-based check:

import "github.com/tetratelabs/wazero/sys"

func isWasmTrap(err error) bool {
    if err == nil {
        return false
    }
    // Normal process exit is not a trap — don't poison the guard.
    var exitErr *sys.ExitError
    if errors.As(err, &exitErr) {
        return exitErr.ExitCode() != 0
    }
    // Fallback for wazero execution traps (e.g., Rust panic → unreachable).
    return strings.Contains(err.Error(), "wasm error:")
}

📐 Best Practice Alignment

4. Consistent Module Naming Convention

The WasmGuard stores a name field and uses it in host_log as [guard:<name>]. This could be more consistent with the project's pkg:filename logger namespace convention if guard names always follow a predictable format. Currently guard names come from serverID (e.g., "github"), which is appropriate — just noting that instantiateHostFunctions doesn't pass the context through host_log in a way that would allow correlating with request traces.


Summary of Recommendations

Priority Action Effort
🔴 Medium Add Registry.Close() and call it during server shutdown Small
🟡 Medium Fix logging namespace confusion in registry.go Trivial
🟡 Medium Use sys.ExitError typed check in isWasmTrap Small

References

  • Module: github.com/tetratelabs/wazero v1.11.0
  • Primary file: internal/guard/wasm.go (1210 lines)
  • Test file: internal/guard/wasm_test.go

Generated by Go Fan 🐹 — Daily Go Module Reviewer
Round-robin state: reviewed [go-sdk → wazero]

Note

🔒 Integrity filter blocked 17 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by Go Fan · ● 4M ·

  • expires on Apr 21, 2026, 7:50 AM UTC

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions