Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423package executor_test
import ( "context" "os" "path/filepath" "strings" "testing" "time"
"tangled.sh/calabro.io/watsm/executor")
func loadTestdata(t *testing.T, name string) []byte { t.Helper() data, err := os.ReadFile(filepath.Join("testdata", name)) if err != nil { t.Fatalf("reading testdata/%s: %v", name, err) } return data}
// ---------------------------------------------------------------------------// Validation// ---------------------------------------------------------------------------
func TestValidate(t *testing.T) { t.Parallel()
tests := []struct { name string input []byte maxSize int64 wantErr string }{ { name: "valid wasm header", input: append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 100)...), maxSize: 1024, }, { name: "too small", input: []byte{0x00, 0x61, 0x73, 0x6d}, maxSize: 1024, wantErr: "too small", }, { name: "bad magic", input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, maxSize: 1024, wantErr: "magic number", }, { name: "too large", input: append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 2000)...), maxSize: 1024, wantErr: "too large", }, { name: "empty", input: []byte{}, maxSize: 1024, wantErr: "too small", }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel()
err := executor.Validate(tt.input, tt.maxSize) if tt.wantErr == "" { if err != nil { t.Fatalf("unexpected error: %v", err) } return } if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error %q does not contain %q", err.Error(), tt.wantErr) } }) }}
// ---------------------------------------------------------------------------// Basic execution// ---------------------------------------------------------------------------
func TestExecuteHelloWorld(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "hello.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if got := strings.TrimSpace(result.Stdout); got != "hello world" { t.Errorf("stdout: got %q, want %q", got, "hello world") } if result.Stderr != "" { t.Errorf("stderr: got %q, want empty", result.Stderr) } if result.DurationMs < 0 { t.Errorf("duration should be non-negative: %d", result.DurationMs) }}
func TestExecuteStderr(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "stderr.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if got := strings.TrimSpace(result.Stdout); got != "out" { t.Errorf("stdout: got %q, want %q", got, "out") } if got := strings.TrimSpace(result.Stderr); got != "err" { t.Errorf("stderr: got %q, want %q", got, "err") }}
func TestExecuteExitError(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "exit_err.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusError { t.Errorf("status: got %q, want %q", result.Status, executor.StatusError) } if result.ExitCode != 42 { t.Errorf("exit code: got %d, want 42", result.ExitCode) }}
// ---------------------------------------------------------------------------// Resource limits// ---------------------------------------------------------------------------
func TestExecuteTimeout(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "spin.wasm") cfg := executor.DefaultConfig() cfg.MaxExecTime = 500 * time.Millisecond
start := time.Now() result, err := executor.Execute(context.Background(), cfg, wasm) elapsed := time.Since(start)
if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusTimeout { t.Errorf("status: got %q, want %q", result.Status, executor.StatusTimeout) }
// Should complete near the timeout, not hang. // Allow generous slack for race detector overhead. if elapsed > 30*time.Second { t.Errorf("took too long: %v (expected near 500ms)", elapsed) }}
func TestExecuteContextCancellation(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "spin.wasm") cfg := executor.DefaultConfig() cfg.MaxExecTime = 10 * time.Second // long timeout — we'll cancel early
ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(300 * time.Millisecond) cancel() }()
start := time.Now() result, err := executor.Execute(ctx, cfg, wasm) elapsed := time.Since(start)
if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusTimeout { t.Errorf("status: got %q, want %q", result.Status, executor.StatusTimeout) }
// Allow generous slack for race detector overhead. if elapsed > 30*time.Second { t.Errorf("took too long: %v (expected near 300ms)", elapsed) }}
func TestExecuteOutputTruncation(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "bigout.wasm") cfg := executor.DefaultConfig() cfg.MaxOutputBytes = 100
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if result.Status != executor.StatusSuccess { t.Errorf("status: got %q, want %q", result.Status, executor.StatusSuccess) } if len(result.Stdout) > 100 { t.Errorf("stdout not truncated: got %d bytes, max 100", len(result.Stdout)) } if len(result.Stdout) < 100 { t.Errorf("stdout shorter than expected: got %d bytes, want 100", len(result.Stdout)) }}
func TestExecuteMemoryLimit(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "allocmem.wasm") cfg := executor.DefaultConfig() cfg.MaxMemoryMB = 16 // 16 MB — module tries to allocate 64 MB
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
// Module should fail to allocate — it will either get an error status // or crash. Either way it must NOT succeed. if strings.Contains(result.Stdout, "ALLOC_OK") { t.Error("module was able to allocate 64 MB despite 16 MB limit") }}
func TestExecuteInvalidWasm(t *testing.T) { t.Parallel()
cfg := executor.DefaultConfig()
_, err := executor.Execute(context.Background(), cfg, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) if err == nil { t.Fatal("expected error for invalid wasm") }}
func TestExecuteOversizedWasm(t *testing.T) { t.Parallel()
cfg := executor.DefaultConfig() cfg.MaxWasmSize = 100
wasm := append([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00}, make([]byte, 200)...) _, err := executor.Execute(context.Background(), cfg, wasm) if err == nil { t.Fatal("expected error for oversized wasm") } if !strings.Contains(err.Error(), "too large") { t.Errorf("error %q does not mention size", err.Error()) }}
// ---------------------------------------------------------------------------// Sandbox isolation// ---------------------------------------------------------------------------
func TestSandboxNoFilesystemAccess(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "readfile.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if strings.Contains(result.Stdout, "LEAKED") { t.Error("module was able to read /etc/passwd — filesystem not sandboxed") } if !strings.Contains(result.Stdout, "BLOCKED") { t.Errorf("expected BLOCKED in stdout, got %q", result.Stdout) }}
func TestSandboxNoEnvironmentVariables(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "readenv.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if strings.Contains(result.Stdout, "ENV_LEAKED") { t.Errorf("module was able to read env vars — sandbox leaking: %s", result.Stdout) } if !strings.Contains(result.Stdout, "ENV_EMPTY") { t.Errorf("expected ENV_EMPTY in stdout, got %q", result.Stdout) }}
func TestSandboxNoArgs(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "readargs.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
if strings.Contains(result.Stdout, "ARGS_LEAKED") { t.Errorf("module was able to read os.Args — sandbox leaking: %s", result.Stdout) } if !strings.Contains(result.Stdout, "ARGS_EMPTY") { t.Errorf("expected ARGS_EMPTY in stdout, got %q", result.Stdout) }}
func TestSandboxFakeClock(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "readtime.wasm") cfg := executor.DefaultConfig()
result, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("execute: %v", err) }
// wazero's fake walltime starts at 2022-01-01T00:00:00Z. // The module prints YEAR:<year>. It must NOT see the real year. if strings.Contains(result.Stdout, "YEAR:2026") { t.Error("module sees real system time — clock not sandboxed") } if !strings.Contains(result.Stdout, "YEAR:2022") { t.Errorf("expected YEAR:2022 (fake clock), got %q", result.Stdout) }}
// ---------------------------------------------------------------------------// Isolation between executions// ---------------------------------------------------------------------------
func TestExecutionsAreIsolated(t *testing.T) { t.Parallel()
wasm := loadTestdata(t, "hello.wasm") cfg := executor.DefaultConfig()
// Run the same module twice. Each should get a fresh runtime. r1, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("first execute: %v", err) } r2, err := executor.Execute(context.Background(), cfg, wasm) if err != nil { t.Fatalf("second execute: %v", err) }
// Both should succeed identically — no state leakage. if r1.Status != executor.StatusSuccess || r2.Status != executor.StatusSuccess { t.Errorf("status: r1=%q r2=%q", r1.Status, r2.Status) } if strings.TrimSpace(r1.Stdout) != strings.TrimSpace(r2.Stdout) { t.Errorf("different stdout across runs: %q vs %q", r1.Stdout, r2.Stdout) }}
// ---------------------------------------------------------------------------// Config defaults// ---------------------------------------------------------------------------
func TestDefaultConfig(t *testing.T) { t.Parallel()
cfg := executor.DefaultConfig() if cfg.MaxExecTime != 5*time.Second { t.Errorf("MaxExecTime: got %v, want 5s", cfg.MaxExecTime) } if cfg.MaxCompileTime != 10*time.Second { t.Errorf("MaxCompileTime: got %v, want 10s", cfg.MaxCompileTime) } if cfg.MaxMemoryMB != 16 { t.Errorf("MaxMemoryMB: got %d, want 16", cfg.MaxMemoryMB) } if cfg.MaxWasmSize != 10*1024*1024 { t.Errorf("MaxWasmSize: got %d, want 10MB", cfg.MaxWasmSize) } if cfg.MaxOutputBytes != 10_000 { t.Errorf("MaxOutputBytes: got %d, want 10000", cfg.MaxOutputBytes) }}