From b62b6c43b7d8c0f29f03b32fb2d8809d9677c35c Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Tue, 3 Dec 2024 16:23:22 -0700 Subject: [PATCH 1/6] Support graceful error handling for host functions --- extism.go | 10 ++++------ extism_test.go | 12 ++++++++---- host.go | 29 +++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/extism.go b/extism.go index 1e8c120..1a3c7a3 100644 --- a/extism.go +++ b/extism.go @@ -110,12 +110,10 @@ type Plugin struct { close []func(ctx context.Context) error extism api.Module - //Runtime *Runtime - //Main Module - module api.Module - Timeout time.Duration - Config map[string]string - // NOTE: maybe we can have some nice methods for getting/setting vars + hostFuncError error + module api.Module + Timeout time.Duration + Config map[string]string Var map[string][]byte AllowedHosts []string AllowedPaths map[string]string diff --git a/extism_test.go b/extism_test.go index 38b3584..58c424b 100644 --- a/extism_test.go +++ b/extism_test.go @@ -259,11 +259,12 @@ func TestHost_simple(t *testing.T) { mult := NewHostFunctionWithStack( "mult", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { a := DecodeI32(stack[0]) b := DecodeI32(stack[1]) stack[0] = EncodeI32(a * b) + return nil }, []ValueType{ValueTypePTR, ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -288,7 +289,7 @@ func TestHost_memory(t *testing.T) { mult := NewHostFunctionWithStack( "to_upper", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { offset := stack[0] buffer, err := plugin.ReadBytes(offset) if err != nil { @@ -306,6 +307,7 @@ func TestHost_memory(t *testing.T) { } stack[0] = offset + return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -332,7 +334,7 @@ func TestHost_multiple(t *testing.T) { green_message := NewHostFunctionWithStack( "hostGreenMessage", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { offset := stack[0] input, err := plugin.ReadString(offset) @@ -350,6 +352,7 @@ func TestHost_multiple(t *testing.T) { } stack[0] = offset + return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -357,7 +360,7 @@ func TestHost_multiple(t *testing.T) { purple_message := NewHostFunctionWithStack( "hostPurpleMessage", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { offset := stack[0] input, err := plugin.ReadString(offset) @@ -375,6 +378,7 @@ func TestHost_multiple(t *testing.T) { } stack[0] = offset + return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, diff --git a/host.go b/host.go index a32b456..0794f8d 100644 --- a/host.go +++ b/host.go @@ -56,7 +56,7 @@ const ( // // To safely decode/encode values from/to the uint64 inputs/ouputs, users are encouraged to use // Extism's EncodeXXX or DecodeXXX functions. -type HostFunctionStackCallback func(ctx context.Context, p *CurrentPlugin, stack []uint64) +type HostFunctionStackCallback func(ctx context.Context, p *CurrentPlugin, stack []uint64) error // HostFunction represents a custom function defined by the host. type HostFunction struct { @@ -237,9 +237,11 @@ func defineCustomHostFunctions(builder wazero.HostModuleBuilder, funcs []HostFun // See: https://github.com/extism/go-sdk/issues/5#issuecomment-1666774486 closure := f.stackCallback - builder.NewFunctionBuilder().WithGoFunction(api.GoFunc(func(ctx context.Context, stack []uint64) { + builder.NewFunctionBuilder().WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, m api.Module, stack []uint64) { if plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin); ok { - closure(ctx, &CurrentPlugin{plugin}, stack) + if err := closure(ctx, &CurrentPlugin{plugin}, stack); err != nil { + plugin.hostFuncError = err + } return } @@ -306,7 +308,7 @@ func instantiateEnvModule(ctx context.Context, rt wazero.Runtime) (api.Module, e WithGoModuleFunction(api.GoModuleFunc(store_u64), []ValueType{ValueTypeI64, ValueTypeI64}, []ValueType{}). Export("store_u64") - hostFunc := func(name string, f interface{}) { + hostFunc := func(name string, f any) { builder.NewFunctionBuilder().WithFunc(f).Export(name) } @@ -317,6 +319,7 @@ func instantiateEnvModule(ctx context.Context, rt wazero.Runtime) (api.Module, e hostFunc("http_status_code", httpStatusCode) hostFunc("http_headers", httpHeaders) hostFunc("get_log_level", getLogLevel) + hostFunc("host_func_get_error", hostFuncGetError) logFunc := func(name string, level LogLevel) { hostFunc(name, func(ctx context.Context, m api.Module, offset uint64) { @@ -448,6 +451,24 @@ func varGet(ctx context.Context, m api.Module, offset uint64) uint64 { panic("Invalid context, `plugin` key not found") } +func hostFuncGetError(ctx context.Context, m api.Module) uint64 { + plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin) + if !ok { + panic("Invalid context, `plugin` key not found") + } + cp := plugin.currentPlugin() + + if plugin.hostFuncError == nil { + return 0 + } + + offset, err := cp.WriteString(plugin.hostFuncError.Error()) + if err != nil { + panic(fmt.Errorf("failed to write host func error to memory: %v", err)) + } + return offset +} + func varSet(ctx context.Context, m api.Module, nameOffset uint64, valueOffset uint64) { plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin) if !ok { From 30c6df400505f529f71234012f9db6918e15d9c3 Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Tue, 3 Dec 2024 17:01:36 -0700 Subject: [PATCH 2/6] Use existing `error_set` function for passing host errors to guest --- extism_test.go | 12 ++++-------- host.go | 43 ++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/extism_test.go b/extism_test.go index 58c424b..38b3584 100644 --- a/extism_test.go +++ b/extism_test.go @@ -259,12 +259,11 @@ func TestHost_simple(t *testing.T) { mult := NewHostFunctionWithStack( "mult", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { a := DecodeI32(stack[0]) b := DecodeI32(stack[1]) stack[0] = EncodeI32(a * b) - return nil }, []ValueType{ValueTypePTR, ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -289,7 +288,7 @@ func TestHost_memory(t *testing.T) { mult := NewHostFunctionWithStack( "to_upper", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { offset := stack[0] buffer, err := plugin.ReadBytes(offset) if err != nil { @@ -307,7 +306,6 @@ func TestHost_memory(t *testing.T) { } stack[0] = offset - return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -334,7 +332,7 @@ func TestHost_multiple(t *testing.T) { green_message := NewHostFunctionWithStack( "hostGreenMessage", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { offset := stack[0] input, err := plugin.ReadString(offset) @@ -352,7 +350,6 @@ func TestHost_multiple(t *testing.T) { } stack[0] = offset - return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, @@ -360,7 +357,7 @@ func TestHost_multiple(t *testing.T) { purple_message := NewHostFunctionWithStack( "hostPurpleMessage", - func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) error { + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { offset := stack[0] input, err := plugin.ReadString(offset) @@ -378,7 +375,6 @@ func TestHost_multiple(t *testing.T) { } stack[0] = offset - return nil }, []ValueType{ValueTypePTR}, []ValueType{ValueTypePTR}, diff --git a/host.go b/host.go index 0794f8d..6ebae48 100644 --- a/host.go +++ b/host.go @@ -56,7 +56,7 @@ const ( // // To safely decode/encode values from/to the uint64 inputs/ouputs, users are encouraged to use // Extism's EncodeXXX or DecodeXXX functions. -type HostFunctionStackCallback func(ctx context.Context, p *CurrentPlugin, stack []uint64) error +type HostFunctionStackCallback func(ctx context.Context, p *CurrentPlugin, stack []uint64) // HostFunction represents a custom function defined by the host. type HostFunction struct { @@ -109,6 +109,24 @@ func (p *Plugin) currentPlugin() *CurrentPlugin { return &CurrentPlugin{p} } +// SetHostFunctionError allows the host function to set an error that will be +// gracefully returned by extism guest modules. +func (p *CurrentPlugin) SetHostFunctionError(ctx context.Context, err error) { + if err == nil { + return + } + + offset, err := p.WriteString(err.Error()) + if err != nil { + panic(fmt.Sprintf("failed to write error message to memory: %v", err)) + } + + _, err = p.plugin.extism.ExportedFunction("error_set").Call(ctx, offset) + if err != nil { + panic(fmt.Sprintf("failed to set error: %v", err)) + } +} + func (p *CurrentPlugin) Log(level LogLevel, message string) { p.plugin.Log(level, message) } @@ -239,9 +257,7 @@ func defineCustomHostFunctions(builder wazero.HostModuleBuilder, funcs []HostFun builder.NewFunctionBuilder().WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, m api.Module, stack []uint64) { if plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin); ok { - if err := closure(ctx, &CurrentPlugin{plugin}, stack); err != nil { - plugin.hostFuncError = err - } + closure(ctx, &CurrentPlugin{plugin}, stack) return } @@ -319,7 +335,6 @@ func instantiateEnvModule(ctx context.Context, rt wazero.Runtime) (api.Module, e hostFunc("http_status_code", httpStatusCode) hostFunc("http_headers", httpHeaders) hostFunc("get_log_level", getLogLevel) - hostFunc("host_func_get_error", hostFuncGetError) logFunc := func(name string, level LogLevel) { hostFunc(name, func(ctx context.Context, m api.Module, offset uint64) { @@ -451,24 +466,6 @@ func varGet(ctx context.Context, m api.Module, offset uint64) uint64 { panic("Invalid context, `plugin` key not found") } -func hostFuncGetError(ctx context.Context, m api.Module) uint64 { - plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin) - if !ok { - panic("Invalid context, `plugin` key not found") - } - cp := plugin.currentPlugin() - - if plugin.hostFuncError == nil { - return 0 - } - - offset, err := cp.WriteString(plugin.hostFuncError.Error()) - if err != nil { - panic(fmt.Errorf("failed to write host func error to memory: %v", err)) - } - return offset -} - func varSet(ctx context.Context, m api.Module, nameOffset uint64, valueOffset uint64) { plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin) if !ok { From 3a64cc06320b666b977ea7fc8176bfd624e37289 Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Wed, 4 Dec 2024 08:13:48 -0700 Subject: [PATCH 3/6] Cleaned up based on PR feedback --- extism.go | 1 - host.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extism.go b/extism.go index 1a3c7a3..bfb917f 100644 --- a/extism.go +++ b/extism.go @@ -110,7 +110,6 @@ type Plugin struct { close []func(ctx context.Context) error extism api.Module - hostFuncError error module api.Module Timeout time.Duration Config map[string]string diff --git a/host.go b/host.go index 6ebae48..c4959ed 100644 --- a/host.go +++ b/host.go @@ -109,9 +109,9 @@ func (p *Plugin) currentPlugin() *CurrentPlugin { return &CurrentPlugin{p} } -// SetHostFunctionError allows the host function to set an error that will be +// SetError allows the host function to set an error that will be // gracefully returned by extism guest modules. -func (p *CurrentPlugin) SetHostFunctionError(ctx context.Context, err error) { +func (p *CurrentPlugin) SetError(ctx context.Context, err error) { if err == nil { return } @@ -255,7 +255,7 @@ func defineCustomHostFunctions(builder wazero.HostModuleBuilder, funcs []HostFun // See: https://github.com/extism/go-sdk/issues/5#issuecomment-1666774486 closure := f.stackCallback - builder.NewFunctionBuilder().WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, m api.Module, stack []uint64) { + builder.NewFunctionBuilder().WithGoFunction(api.GoFunc(func(ctx context.Context, stack []uint64) { if plugin, ok := ctx.Value(PluginCtxKey("plugin")).(*Plugin); ok { closure(ctx, &CurrentPlugin{plugin}, stack) return From 261a41b561c79323762f73d22a3e5e3cbf65da0f Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Wed, 4 Dec 2024 11:45:49 -0700 Subject: [PATCH 4/6] wip approach to signal host->host errors --- extism.go | 7 +++++++ host.go | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/extism.go b/extism.go index bfb917f..0695cda 100644 --- a/extism.go +++ b/extism.go @@ -1,6 +1,7 @@ package extism import ( + "bytes" "context" "crypto/sha256" _ "embed" @@ -432,6 +433,12 @@ func (p *Plugin) GetErrorWithContext(ctx context.Context) string { } mem, _ := p.Memory().Read(uint32(errOffs[0]), uint32(errLen[0])) + if len(mem) < 2 { + return "" + } + if bytes.Equal(mem[:2], []byte{0xff, 0xff}) { + return "" + } return string(mem) } diff --git a/host.go b/host.go index c4959ed..9000acc 100644 --- a/host.go +++ b/host.go @@ -116,7 +116,8 @@ func (p *CurrentPlugin) SetError(ctx context.Context, err error) { return } - offset, err := p.WriteString(err.Error()) + b := []byte(err.Error()) + offset, err := p.WriteBytes(append([]byte{0xff, 0xff}, b...)) if err != nil { panic(fmt.Sprintf("failed to write error message to memory: %v", err)) } From 78228e7c699c0e44e5b9b0d35a6b34bfbf6d88b7 Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Thu, 5 Dec 2024 15:49:28 -0700 Subject: [PATCH 5/6] Shore up graceful host function error handling --- errors.go | 74 ++++++++++++++++++++++++++++++ errors_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ extism.go | 12 ++--- extism_test.go | 49 ++++++++++++++++++++ host.go | 3 +- plugin.go | 8 +++- 6 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 errors.go create mode 100644 errors_test.go diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..29d1fae --- /dev/null +++ b/errors.go @@ -0,0 +1,74 @@ +package extism + +// errPrefix is a sentinel byte sequence used to identify errors originating from host functions. +// It helps distinguish these errors when serialized to bytes. +var errPrefix = []byte{0xFF, 0xFE, 0xFD} + +// hostFuncError wraps another error and identifies it as a host function error. +// When a host function is called and that host function wants to return an error, +// internally extism will wrap that error in this type before serializing the error +// using the bytes method, and writing the error into WASM memory so that the guest +// can read the error. +// +// The bytes method appends a set of sentinel bytes which the host can later read +// when calls `error_get` to see if the error that was previously set was set by +// the host or the guest. If we see the matching sentinel bytes in the prefix of +// the error bytes, then we know that the error was a host function error, and the +// host can ignore it. +// +// The purpose of this is to allow us to piggyback off the existing `error_get` and +// `error_set` extism kernel functions. These previously were only used by guests to +// communicate errors to the host. In order to prevent host plugin function calls from +// seeing their own host function errors, the plugin can check and see if the error +// was created via a host function using this type. +// +// This is an effort to preserve backwards compatibility with existing PDKs which +// may not know to call `error_get` to see if there are any host->guest errors. We +// need the host SDKs to handle the scenario where the host calls `error_set` but +// the guest never calls `error_get` resulting in the host seeing their own error. +type hostFuncError struct { + inner error // The underlying error being wrapped. +} + +// Error implements the error interface for hostFuncError. +// It returns the message of the wrapped error or an empty string if there is no inner error. +func (e *hostFuncError) Error() string { + if e.inner == nil { + return "" + } + return e.inner.Error() +} + +// bytes serializes the hostFuncError into a byte slice. +// If there is no inner error, it returns nil. Otherwise, it prefixes the error message +// with a sentinel byte sequence to facilitate identification during deserialization. +func (e *hostFuncError) bytes() []byte { + if e.inner == nil { + return nil + } + return append(errPrefix, []byte(e.inner.Error())...) +} + +// isHostFuncError checks if the given byte slice represents a serialized host function error. +// It verifies the presence of the sentinel prefix to make this determination. +func isHostFuncError(error []byte) bool { + if error == nil { + return false + } + if len(error) < len(errPrefix) { + return false // The slice is too short to contain the prefix. + } + // Check if the slice starts with the sentinel prefix. + return error[0] == errPrefix[0] && + error[1] == errPrefix[1] && + error[2] == errPrefix[2] +} + +// newHostFuncError creates a new hostFuncError instance wrapping the provided error. +// If the input error is nil, it returns nil to avoid creating redundant wrappers. +func newHostFuncError(err error) *hostFuncError { + if err == nil { + return nil + } + return &hostFuncError{inner: err} +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..d57111b --- /dev/null +++ b/errors_test.go @@ -0,0 +1,120 @@ +package extism + +import ( + "bytes" + "errors" + "testing" +) + +func TestNewHostFuncError(t *testing.T) { + tests := []struct { + name string + inputErr error + wantNil bool + }{ + { + name: "nil error input", + inputErr: nil, + wantNil: true, + }, + { + name: "non-nil error input", + inputErr: errors.New("test error"), + wantNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := newHostFuncError(tt.inputErr) + if (err == nil) != tt.wantNil { + t.Errorf("got nil: %v, want nil: %v", err == nil, tt.wantNil) + } + }) + } +} + +func TestBytes(t *testing.T) { + tests := []struct { + name string + inputErr error + wantPrefix []byte + wantMsg string + wantNil bool + }{ + { + name: "nil inner error", + inputErr: nil, + wantPrefix: nil, + wantMsg: "", + wantNil: true, + }, + { + name: "non-nil inner error", + inputErr: errors.New("test error"), + wantPrefix: errPrefix, + wantMsg: "test error", + wantNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &hostFuncError{inner: tt.inputErr} + b := e.bytes() + + if tt.wantNil { + if b != nil { + t.Errorf("expected nil, got %x", b) + } + return + } + + if len(b) < len(tt.wantPrefix) { + t.Fatalf("returned bytes too short, got %x, want prefix %x", b, tt.wantPrefix) + } + + if !bytes.HasPrefix(b, tt.wantPrefix) { + t.Errorf("expected prefix %x, got %x", tt.wantPrefix, b[:len(tt.wantPrefix)]) + } + + gotMsg := string(b[len(tt.wantPrefix):]) + if gotMsg != tt.wantMsg { + t.Errorf("expected message %q, got %q", tt.wantMsg, gotMsg) + } + }) + } +} + +func TestIsHostFuncError(t *testing.T) { + tests := []struct { + name string + inputErr []byte + want bool + }{ + { + name: "nil error input", + inputErr: nil, + want: false, + }, + { + name: "not a hostFuncError", + inputErr: []byte("normal error"), + want: false, + }, + { + name: "valid hostFuncError", + inputErr: newHostFuncError(errors.New("host function error")).bytes(), + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isHostFuncError(tt.inputErr) + if got != tt.want { + t.Errorf("isHostFuncError(%v) = %v, want %v", tt.inputErr, got, tt.want) + } + }) + } +} diff --git a/extism.go b/extism.go index 0695cda..0ec4fdd 100644 --- a/extism.go +++ b/extism.go @@ -1,7 +1,6 @@ package extism import ( - "bytes" "context" "crypto/sha256" _ "embed" @@ -433,12 +432,15 @@ func (p *Plugin) GetErrorWithContext(ctx context.Context) string { } mem, _ := p.Memory().Read(uint32(errOffs[0]), uint32(errLen[0])) - if len(mem) < 2 { - return "" - } - if bytes.Equal(mem[:2], []byte{0xff, 0xff}) { + + // A host function error is an error set by a host function during a guest->host function + // call. These errors are intended to be handled only by the guest. If the error makes it + // back here, the guest PDK most likely doesn't know to handle it, in which case we should + // ignore it here. + if isHostFuncError(mem) { return "" } + return string(mem) } diff --git a/extism_test.go b/extism_test.go index 38b3584..6135b9f 100644 --- a/extism_test.go +++ b/extism_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" observe "github.com/dylibso/observe-sdk/go" "github.com/dylibso/observe-sdk/go/adapter/stdout" @@ -1038,6 +1039,54 @@ func TestEnableExperimentalFeature(t *testing.T) { } } +// This test creates host functions that set errors. Previously, host functions +// would have to panic to communicate host function errors, but this unfortunately +// stopped execution of the guest function. In other words, there was no way to +// gracefully communicate errors from host->guest when the guest called a host +// function. This has since been fixed and this test proves that even when guests +// don't reset the error state, the host can still determine that the current error +// state was a host->guest error and not a guest->host error and ignores it. +func TestHostFunctionError(t *testing.T) { + manifest := manifest("host_multiple.wasm") + + hostGreenMessage := NewHostFunctionWithStack( + "hostGreenMessage", + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + plugin.SetError(ctx, errors.New("this is an error")) + }, + []ValueType{ValueTypePTR}, + []ValueType{ValueTypePTR}, + ) + hostPurpleMessage := NewHostFunctionWithStack( + "hostPurpleMessage", + func(ctx context.Context, plugin *CurrentPlugin, stack []uint64) { + plugin.SetError(ctx, errors.New("this is an error")) + }, + []ValueType{ValueTypePTR}, + []ValueType{ValueTypePTR}, + ) + + ctx := context.Background() + p, err := NewCompiledPlugin(ctx, manifest, PluginConfig{ + EnableWasi: true, + }, []HostFunction{ + hostGreenMessage, + hostPurpleMessage, + }) + require.NoError(t, err) + + pluginInst, err := p.Instance(ctx, PluginInstanceConfig{ + ModuleConfig: wazero.NewModuleConfig().WithSysWalltime(), + }) + require.NoError(t, err) + + _, _, err = pluginInst.Call( + "say_green", + []byte("John Doe"), + ) + require.NoError(t, err, "the host function should have returned an error to the guest but it should not have propagated back to the host") +} + func BenchmarkInitialize(b *testing.B) { ctx := context.Background() cache := wazero.NewCompilationCache() diff --git a/host.go b/host.go index 9000acc..9c85136 100644 --- a/host.go +++ b/host.go @@ -116,8 +116,7 @@ func (p *CurrentPlugin) SetError(ctx context.Context, err error) { return } - b := []byte(err.Error()) - offset, err := p.WriteBytes(append([]byte{0xff, 0xff}, b...)) + offset, err := p.WriteBytes(newHostFuncError(err).bytes()) if err != nil { panic(fmt.Sprintf("failed to write error message to memory: %v", err)) } diff --git a/plugin.go b/plugin.go index c1139ab..99f8673 100644 --- a/plugin.go +++ b/plugin.go @@ -2,6 +2,7 @@ package extism import ( "context" + "encoding/base64" "errors" "fmt" observe "github.com/dylibso/observe-sdk/go" @@ -220,7 +221,12 @@ func (p *CompiledPlugin) Instance(ctx context.Context, config PluginInstanceConf if moduleConfig == nil { moduleConfig = wazero.NewModuleConfig() } - moduleConfig = moduleConfig.WithName(strconv.Itoa(int(p.instanceCount.Add(1)))) + moduleConfig = moduleConfig. + WithName(strconv.Itoa(int(p.instanceCount.Add(1)))). + // We can tell the guest module what the error prefix will be for errors that are set + // by host functions. Guests should trim this prefix off of their error messages when + // reading them. + WithEnv("EXTISM_HOST_FUNC_ERROR_PREFIX", base64.StdEncoding.EncodeToString(errPrefix)) // NOTE: this is only necessary for guest modules because // host modules have the same access privileges as the host itself From ebfdc417fb3f6ca7dc29ff3634b873ed1dbd5454 Mon Sep 17 00:00:00 2001 From: Clark McCauley Date: Thu, 5 Dec 2024 17:08:24 -0700 Subject: [PATCH 6/6] Fixed potential panic point if errPrefix were to be change --- errors.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index 29d1fae..d4369ff 100644 --- a/errors.go +++ b/errors.go @@ -1,5 +1,7 @@ package extism +import "bytes" + // errPrefix is a sentinel byte sequence used to identify errors originating from host functions. // It helps distinguish these errors when serialized to bytes. var errPrefix = []byte{0xFF, 0xFE, 0xFD} @@ -58,10 +60,7 @@ func isHostFuncError(error []byte) bool { if len(error) < len(errPrefix) { return false // The slice is too short to contain the prefix. } - // Check if the slice starts with the sentinel prefix. - return error[0] == errPrefix[0] && - error[1] == errPrefix[1] && - error[2] == errPrefix[2] + return bytes.Equal(error[:len(errPrefix)], errPrefix) } // newHostFuncError creates a new hostFuncError instance wrapping the provided error.