mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
The guest's linear memory was taken from mod.Memory() and used unchecked, so an untrusted plugin wasm with no memory section nil-dereferenced on the unrecovered startup path and crashed the server. All guest-memory access now goes through one guestMemory() helper that detects wazero's non-nil interface wrapping a nil *MemoryInstance, binding no commands at activation and returning the existing missing-export diagnostic on dispatch. Verified by a panel of agents; the added regression test panics with the finding's exact stack against the unpatched tree. Note: TestRegistry_Activate_WithoutRuntime and TestRegistry_EnablePlugin_RollsBackWhenActivationFails fail under -tags wazero, confirmed here to fail identically on the base tree. They are pre-existing and unrelated; CI builds the wazero variant but does not test it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
463 lines
16 KiB
Go
463 lines
16 KiB
Go
//go:build wazero
|
|
|
|
// Phase C Step 9 — Wazero runtime integration tests. Only compiled with
|
|
// `-tags wazero`, alongside sandbox_wazero.go. These tests exercise the
|
|
// behaviours the default build cannot:
|
|
//
|
|
// - NewRegistry stands up a real wazero.Runtime
|
|
// - activateWithRuntime compiles a real .wasm module and tracks the
|
|
// resulting instance on the Instance struct
|
|
// - EnablePlugin takes a manifest from a PluginStore row through
|
|
// activation end-to-end
|
|
// - invokeCommand gracefully returns a user-facing error when the plugin
|
|
// does not export command_dispatch
|
|
// - Close tears the runtime down without panicking
|
|
//
|
|
// Test fixture: the 41-byte `add.wasm` module from the wazero examples, a
|
|
// trivial module that exports `add(i32,i32) -> i32`. It does NOT export
|
|
// `command_dispatch`, which is intentional — the command path must handle
|
|
// that case.
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// addWASM is the bytes of a minimal (module (func (export "add") ... )).
|
|
// Verified against the wazero examples fixture; 41 bytes. Using a literal
|
|
// here avoids dragging a binary asset into the repo.
|
|
var addWASM = []byte{
|
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01,
|
|
0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
|
|
0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09,
|
|
0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a,
|
|
0x0b,
|
|
}
|
|
|
|
func writeTestPlugin(t *testing.T, root, name string, manifest string, wasmBytes []byte) {
|
|
t.Helper()
|
|
pluginDir := filepath.Join(root, name)
|
|
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(manifest), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(pluginDir, "hello.wasm"), wasmBytes, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func newWazeroTestRegistry(t *testing.T, dir string) (*Registry, PluginStore) {
|
|
t.Helper()
|
|
mem := openPluginTestDB(t)
|
|
reg, err := NewRegistry(Config{
|
|
Directory: dir,
|
|
MaxMemoryMB: 16,
|
|
CPUBudgetMs: 100,
|
|
Store: mem,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewRegistry: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = reg.Close(context.Background()) })
|
|
return reg, mem
|
|
}
|
|
|
|
func TestWazeroRegistryCreatesRuntime(t *testing.T) {
|
|
reg, _ := newWazeroTestRegistry(t, t.TempDir())
|
|
if reg.runtimePlatform == nil {
|
|
t.Fatal("expected wazero runtime to be wired in tagged build")
|
|
}
|
|
if reg.platformClose == nil {
|
|
t.Fatal("expected platformClose to be set")
|
|
}
|
|
}
|
|
|
|
func TestWazeroActivateCompilesModule(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}`
|
|
writeTestPlugin(t, dir, "hello", manifest, addWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatalf("LoadAll: %v", err)
|
|
}
|
|
|
|
rows, err := mem.ListPlugins(ctx)
|
|
if err != nil || len(rows) != 1 {
|
|
t.Fatalf("ListPlugins: rows=%+v err=%v", rows, err)
|
|
}
|
|
|
|
// The plugin starts disabled; enable it and confirm the module compiles.
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if inst == nil {
|
|
t.Fatal("instance not registered after enable")
|
|
}
|
|
if !inst.Enabled {
|
|
t.Fatal("instance should be enabled after EnablePlugin")
|
|
}
|
|
if inst.module == nil {
|
|
t.Fatal("expected inst.module to be populated after activation")
|
|
}
|
|
}
|
|
|
|
func TestWazeroDispatchCommandMissingExport(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}`
|
|
writeTestPlugin(t, dir, "hello", manifest, addWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if err := reg.RegisterCommand("hello", inst); err != nil {
|
|
t.Fatalf("RegisterCommand: %v", err)
|
|
}
|
|
|
|
result, ok := reg.DispatchCommand(ctx, 1, 2, "hello", nil)
|
|
if !ok {
|
|
t.Fatal("expected DispatchCommand to return a result")
|
|
}
|
|
if result == nil || result.Reply == "" {
|
|
t.Fatal("expected a non-empty reply when export is missing")
|
|
}
|
|
}
|
|
|
|
func TestWazeroCloseTearsDownRuntime(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}`
|
|
writeTestPlugin(t, dir, "hello", manifest, addWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
|
|
if err := reg.Close(ctx); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
if reg.runtimePlatform != nil || reg.platformClose != nil {
|
|
t.Fatal("Close should clear platform fields")
|
|
}
|
|
// Calling Close twice must not panic.
|
|
if err := reg.Close(ctx); err != nil {
|
|
t.Fatalf("Close (second): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWazeroDisablePluginFreesModule(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"hello","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"hello"}]}`
|
|
writeTestPlugin(t, dir, "hello", manifest, addWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if inst.module == nil {
|
|
t.Fatal("pre-condition: module should be populated after EnablePlugin")
|
|
}
|
|
if err := reg.DisablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("DisablePlugin: %v", err)
|
|
}
|
|
if inst.module != nil {
|
|
t.Fatal("DisablePlugin must release the wazero module (inst.module != nil)")
|
|
}
|
|
if inst.Enabled {
|
|
t.Fatal("DisablePlugin must clear inst.Enabled")
|
|
}
|
|
// Re-enabling must rebuild a new module.
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("re-EnablePlugin: %v", err)
|
|
}
|
|
if inst.module == nil {
|
|
t.Fatal("re-Enable should repopulate inst.module")
|
|
}
|
|
}
|
|
|
|
// spinWASM implements the command-dispatch ABI with input-dependent runtime:
|
|
//
|
|
// (module
|
|
// (memory (export "memory") 1)
|
|
// (func (export "allocate") (param i32) (result i32) i32.const 8)
|
|
// (func (export "deallocate") (param i32 i32))
|
|
// (func (export "command_dispatch") (param i32 i32) (result i32 i32)
|
|
// local.get 1 ;; payload length
|
|
// i32.const 100
|
|
// i32.gt_u
|
|
// if (loop br 0 end) end ;; payloads over 100 bytes spin forever
|
|
// i32.const 0 i32.const 0))
|
|
//
|
|
// A dispatch with no args stays under 100 payload bytes and returns
|
|
// immediately; long args push the JSON payload over 100 bytes and trigger an
|
|
// infinite loop, which the CPU budget must interrupt.
|
|
var spinWASM = []byte{
|
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // \0asm v1
|
|
// type section: (i32)->i32, (i32,i32)->(), (i32,i32)->(i32,i32)
|
|
0x01, 0x12, 0x03,
|
|
0x60, 0x01, 0x7f, 0x01, 0x7f,
|
|
0x60, 0x02, 0x7f, 0x7f, 0x00,
|
|
0x60, 0x02, 0x7f, 0x7f, 0x02, 0x7f, 0x7f,
|
|
// function section: 3 funcs using types 0,1,2
|
|
0x03, 0x04, 0x03, 0x00, 0x01, 0x02,
|
|
// memory section: 1 page, no max
|
|
0x05, 0x03, 0x01, 0x00, 0x01,
|
|
// export section: memory, allocate, deallocate, command_dispatch
|
|
0x07, 0x35, 0x04,
|
|
0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00,
|
|
0x08, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00,
|
|
0x0a, 0x64, 0x65, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x65, 0x00, 0x01,
|
|
0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x00, 0x02,
|
|
// code section
|
|
0x0a, 0x1e, 0x03,
|
|
// allocate: return 8
|
|
0x04, 0x00, 0x41, 0x08, 0x0b,
|
|
// deallocate: nop
|
|
0x02, 0x00, 0x0b,
|
|
// command_dispatch: spin if len>100 else return (0,0)
|
|
0x14, 0x00,
|
|
0x20, 0x01, // local.get 1
|
|
0x41, 0xe4, 0x00, // i32.const 100
|
|
0x4b, // i32.gt_u
|
|
0x04, 0x40, // if
|
|
0x03, 0x40, // loop
|
|
0x0c, 0x00, // br 0
|
|
0x0b, // end loop
|
|
0x0b, // end if
|
|
0x41, 0x00, // i32.const 0
|
|
0x41, 0x00, // i32.const 0
|
|
0x0b, // end
|
|
}
|
|
|
|
// TestWazeroCPUBudgetOverrunDoesNotBrickPlugin locks in the W1-1 fix: an
|
|
// over-budget command must return the budget error, and the SAME plugin must
|
|
// serve the next command via lazy re-instantiation — not stay dead until an
|
|
// admin disable/enable cycle or server restart.
|
|
func TestWazeroCPUBudgetOverrunDoesNotBrickPlugin(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"spinner","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"spin"}]}`
|
|
writeTestPlugin(t, dir, "spinner", manifest, spinWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if err := reg.RegisterCommand("spin", inst); err != nil {
|
|
t.Fatalf("RegisterCommand: %v", err)
|
|
}
|
|
|
|
// Baseline: a small payload dispatches fine.
|
|
result, ok := reg.DispatchCommand(ctx, 1, 2, "spin", nil)
|
|
if !ok || result == nil {
|
|
t.Fatalf("baseline dispatch failed: ok=%v result=%+v", ok, result)
|
|
}
|
|
if strings.Contains(result.Reply, "CPU budget") {
|
|
t.Fatalf("baseline dispatch should not hit the budget: %q", result.Reply)
|
|
}
|
|
|
|
// Overrun: a long arg pushes the payload over the spin threshold; the
|
|
// 100ms budget must interrupt it and surface the budget error.
|
|
result, ok = reg.DispatchCommand(ctx, 1, 2, "spin", []string{strings.Repeat("x", 200)})
|
|
if !ok || result == nil {
|
|
t.Fatalf("overrun dispatch returned no result: ok=%v", ok)
|
|
}
|
|
if !strings.Contains(result.Reply, "CPU budget") {
|
|
t.Fatalf("expected CPU budget error, got %q", result.Reply)
|
|
}
|
|
|
|
// The plugin must still work: the next small dispatch re-instantiates the
|
|
// module lazily instead of dispatching into the closed one forever.
|
|
result, ok = reg.DispatchCommand(ctx, 1, 2, "spin", nil)
|
|
if !ok || result == nil {
|
|
t.Fatalf("post-overrun dispatch failed: ok=%v result=%+v", ok, result)
|
|
}
|
|
if strings.Contains(result.Reply, "CPU budget") || strings.Contains(result.Reply, "module closed") {
|
|
t.Fatalf("plugin still bricked after overrun: %q", result.Reply)
|
|
}
|
|
if !inst.Enabled {
|
|
t.Fatal("overrun must not disable the plugin")
|
|
}
|
|
}
|
|
|
|
// TestWazeroConcurrentDispatchRace locks F2: concurrent invocations of the same
|
|
// plugin command must be serialized per Instance. Without a per-Instance lock,
|
|
// two goroutines drive one shared wazero module (allocate / mem.Write /
|
|
// command_dispatch / mem.Read) with no synchronization, racing on its linear
|
|
// memory. Run under -race: without the fix the detector reports a data race;
|
|
// with it the run is clean.
|
|
func TestWazeroConcurrentDispatchRace(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"spinner","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"spin"}]}`
|
|
writeTestPlugin(t, dir, "spinner", manifest, spinWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if err := reg.RegisterCommand("spin", inst); err != nil {
|
|
t.Fatalf("RegisterCommand: %v", err)
|
|
}
|
|
|
|
const goroutines = 8
|
|
var wg sync.WaitGroup
|
|
start := make(chan struct{})
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
for j := 0; j < 20; j++ {
|
|
reg.DispatchCommand(ctx, 1, 2, "spin", nil)
|
|
}
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
}
|
|
|
|
// noMemWASM exports the command ABI but declares NO memory section, so
|
|
// api.Module.Memory() returns nil for it:
|
|
//
|
|
// (module
|
|
// (func (export "list_commands") (result i32 i32) i32.const 0 i32.const 0)
|
|
// (func (export "allocate") (param i32) (result i32) i32.const 0)
|
|
// (func (export "command_dispatch") (param i32 i32) (result i32 i32)
|
|
// i32.const 0 i32.const 0))
|
|
var noMemWASM = []byte{
|
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // \0asm v1
|
|
// type section: ()->(i32,i32), (i32)->i32, (i32,i32)->(i32,i32)
|
|
0x01, 0x12, 0x03,
|
|
0x60, 0x00, 0x02, 0x7f, 0x7f,
|
|
0x60, 0x01, 0x7f, 0x01, 0x7f,
|
|
0x60, 0x02, 0x7f, 0x7f, 0x02, 0x7f, 0x7f,
|
|
// function section: 3 funcs using types 0,1,2
|
|
0x03, 0x04, 0x03, 0x00, 0x01, 0x02,
|
|
// (no memory section — this is the point of the fixture)
|
|
// export section: list_commands, allocate, command_dispatch
|
|
0x07, 0x2f, 0x03,
|
|
0x0d, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x00, 0x00,
|
|
0x08, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x65, 0x00, 0x01,
|
|
0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x00, 0x02,
|
|
// code section
|
|
0x0a, 0x14, 0x03,
|
|
0x06, 0x00, 0x41, 0x00, 0x41, 0x00, 0x0b, // list_commands: (0,0)
|
|
0x04, 0x00, 0x41, 0x00, 0x0b, // allocate: 0
|
|
0x06, 0x00, 0x41, 0x00, 0x41, 0x00, 0x0b, // command_dispatch: (0,0)
|
|
}
|
|
|
|
// TestWazeroMemorylessModuleDoesNotPanic locks the nil-memory guard: a guest
|
|
// that exports the command ABI but declares no memory section must be handled
|
|
// as a bad plugin, not dereferenced. Both host paths that touch guest memory
|
|
// are covered — activation (list_commands) and dispatch (command_dispatch) —
|
|
// because a panic on the activation path aborts server startup for every
|
|
// subsequent restart while the plugin row stays enabled.
|
|
func TestWazeroMemorylessModuleDoesNotPanic(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"nomem","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"],"commands":[{"name":"noop"}]}`
|
|
writeTestPlugin(t, dir, "nomem", manifest, noMemWASM)
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
// Activation calls list_commands, which used to nil-deref the guest memory.
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err != nil {
|
|
t.Fatalf("EnablePlugin: %v", err)
|
|
}
|
|
reg.mu.RLock()
|
|
inst := reg.plugins[rows[0].ID]
|
|
reg.mu.RUnlock()
|
|
if inst == nil || inst.module == nil {
|
|
t.Fatal("expected the module to activate")
|
|
}
|
|
// No command may bind: the name list is unreadable without memory.
|
|
reg.mu.RLock()
|
|
_, bound := reg.commands["noop"]
|
|
reg.mu.RUnlock()
|
|
if bound {
|
|
t.Fatal("memoryless module must not auto-bind commands")
|
|
}
|
|
|
|
// The dispatch path must report a diagnostic instead of writing into nil.
|
|
if err := reg.RegisterCommand("noop", inst); err != nil {
|
|
t.Fatalf("RegisterCommand: %v", err)
|
|
}
|
|
result, ok := reg.DispatchCommand(ctx, 1, 2, "noop", nil)
|
|
if !ok || result == nil {
|
|
t.Fatalf("expected a dispatch result: ok=%v result=%+v", ok, result)
|
|
}
|
|
if !strings.Contains(result.Reply, "no exported memory") {
|
|
t.Fatalf("expected a missing-memory diagnostic, got %q", result.Reply)
|
|
}
|
|
}
|
|
|
|
func TestWazeroInvalidWASMFailsActivation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
manifest := `{"name":"brokey","version":"0.1.0","entrypoint":"hello.wasm","permissions":["commands"]}`
|
|
writeTestPlugin(t, dir, "brokey", manifest, []byte("not a wasm"))
|
|
|
|
reg, mem := newWazeroTestRegistry(t, dir)
|
|
ctx := context.Background()
|
|
if err := reg.LoadAll(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, _ := mem.ListPlugins(ctx)
|
|
if err := reg.EnablePlugin(ctx, rows[0].ID); err == nil {
|
|
t.Fatal("expected EnablePlugin to fail on invalid WASM")
|
|
}
|
|
}
|