Appearance
Add limits and cancellation
There are three separate boundaries:
RuntimeConfigcontrols compilation and runtime-wide resource ceilings.Policychecks one module when an instance is created.context.Contextcontrols how long instantiation and calls may continue.
Run with explicit boundaries
Use the module.wasm from Run WebAssembly from Go. Replace main.go with:
go
package main
import (
"context"
"fmt"
"log"
"os"
"slices"
"time"
"github.com/wago-org/wago"
)
func run() error {
wasm, err := os.ReadFile("module.wasm")
if err != nil {
return err
}
config := wago.NewRuntimeConfig().
WithMaxModuleBytes(16<<20).
WithMemoryLimitPages(256).
WithMaxFunctionLocals(4096).
WithInstanceLimits(8, 0)
if err := config.Validate(); err != nil {
return err
}
runtime := wago.NewRuntime(wago.WithRuntimeConfig(config))
defer runtime.Close()
module, err := runtime.Compile(wasm)
if err != nil {
return err
}
defer module.Close()
policy := wago.Policy{
MaxMemories: 1,
MaxTableEntries: 1024,
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
instance, err := runtime.Instantiate(ctx, module, wago.WithPolicy(policy))
if err != nil {
return err
}
defer instance.Close()
if slices.Contains(module.Exports(), "_initialize") {
if _, err := instance.InvokeContext(ctx, "_initialize"); err != nil {
return err
}
}
results, err := instance.InvokeContext(ctx, "add", wago.I32(20), wago.I32(22))
if err != nil {
return err
}
fmt.Println(wago.AsI32(results[0]))
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}sh
go run .text
42WithMaxModuleBytes rejects oversized input before compilation. WithMemoryLimitPages caps the live size of each linear memory. WithInstanceLimits bounds simultaneously live direct instances; a closed instance returns its admission budget.
Policy is an admission check against the module's declared capabilities and limits. The zero policy is permissive. A non-empty AllowedCapabilities list is exclusive, and DeniedCapabilities always wins. A MaxMemoryBytes policy also requires the module to declare a finite maximum.
Policy.MaxInvokeDuration is retained for compatibility but is not enforced: a nonzero value is rejected with wago.ErrUnsupported. Use a context deadline for call duration.
Handle cancellation and failures
InvokeContext and InvokeValues return context.Canceled or context.DeadlineExceeded when the supplied context interrupts guest execution:
go
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("guest exceeded its deadline: %w", err)
}Policy failures wrap wago.ErrPermissionDenied. Runtime traps can be inspected without matching error text:
go
var trap *wago.TrapError
if errors.As(err, &trap) {
log.Printf("guest trapped: %s", trap.Code)
}A context can interrupt Wasm execution, but it cannot forcibly make arbitrary Go code return. Host functions that may block should accept a bounded dependency or cooperate with cancellation. Truly hostile blocking code needs process isolation.
Interrupt a guest that does not return
A fast add call rarely reaches its deadline. This test needs WABT's wat2wasm, regardless of your earlier guest language. To check your actual cancellation path, create guest/loop.wat:
wat
(module
(func (export "spin")
(loop $again
br $again))
(func (export "answer") (result i32)
i32.const 42))sh
wat2wasm guest/loop.wat -o loop.wasmReplace main.go with this complete program. Instantiation happens outside the short call deadline, so the timeout tests guest execution rather than setup:
go
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"github.com/wago-org/wago"
)
func run() error {
wasm, err := os.ReadFile("loop.wasm")
if err != nil {
return err
}
runtime := wago.NewRuntime()
defer runtime.Close()
module, err := runtime.Compile(wasm)
if err != nil {
return err
}
defer module.Close()
instance, err := runtime.Instantiate(context.Background(), module)
if err != nil {
return err
}
defer instance.Close()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
_, err = instance.InvokeContext(ctx, "spin")
if !errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("expected deadline exceeded, got %v", err)
}
fmt.Println("spin stopped at its deadline")
out, err := instance.InvokeContext(context.Background(), "answer")
if err != nil {
return err
}
fmt.Println("next call:", wago.AsI32(out[0]))
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}sh
go run .text
spin stopped at its deadline
next call: 42Cancellation does not roll back memory or globals, and it does not automatically close the instance. Use a new instance if your application needs a clean state after an interrupted call.
Native cancellation is supported on amd64 and arm64 with standard Go, or TinyGo with -scheduler=threads. Other TinyGo schedulers reject cancelable native calls before entering the guest. On Linux/amd64 with standard Go, interruption uses a thread-directed signal; other supported targets stop at native safepoints. Do not turn a deadline into a hard wall-clock guarantee for arbitrary host callbacks.
Runtime.Compile does not take a context. Apply compilation-size and resource limits before accepting untrusted modules, and account for compilation separately from invocation timeouts.
