fix: should not panic when a module is instantiated with a closed runtime (#1089)
fixes #1088 Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
This commit is contained in:
33
runtime.go
33
runtime.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/tetratelabs/wazero/api"
|
"github.com/tetratelabs/wazero/api"
|
||||||
experimentalapi "github.com/tetratelabs/wazero/experimental"
|
experimentalapi "github.com/tetratelabs/wazero/experimental"
|
||||||
@@ -122,6 +123,7 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
|
|||||||
engine = config.newEngine(ctx, config.enabledFeatures, nil)
|
engine = config.newEngine(ctx, config.enabledFeatures, nil)
|
||||||
}
|
}
|
||||||
store := wasm.NewStore(config.enabledFeatures, engine)
|
store := wasm.NewStore(config.enabledFeatures, engine)
|
||||||
|
zero := uint64(0)
|
||||||
return &runtime{
|
return &runtime{
|
||||||
cache: cacheImpl,
|
cache: cacheImpl,
|
||||||
store: store,
|
store: store,
|
||||||
@@ -130,6 +132,7 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
|
|||||||
memoryCapacityFromMax: config.memoryCapacityFromMax,
|
memoryCapacityFromMax: config.memoryCapacityFromMax,
|
||||||
dwarfDisabled: config.dwarfDisabled,
|
dwarfDisabled: config.dwarfDisabled,
|
||||||
storeCustomSections: config.storeCustomSections,
|
storeCustomSections: config.storeCustomSections,
|
||||||
|
closed: &zero,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +145,14 @@ type runtime struct {
|
|||||||
memoryCapacityFromMax bool
|
memoryCapacityFromMax bool
|
||||||
dwarfDisabled bool
|
dwarfDisabled bool
|
||||||
storeCustomSections bool
|
storeCustomSections bool
|
||||||
|
|
||||||
|
// closed is the pointer used both to guard moduleEngine.CloseWithExitCode and to store the exit code.
|
||||||
|
//
|
||||||
|
// The update value is 1 + exitCode << 32. This ensures an exit code of zero isn't mistaken for never closed.
|
||||||
|
//
|
||||||
|
// Note: Exclusively reading and updating this with atomics guarantees cross-goroutine observations.
|
||||||
|
// See /RATIONALE.md
|
||||||
|
closed *uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module implements Runtime.Module.
|
// Module implements Runtime.Module.
|
||||||
@@ -151,6 +162,10 @@ func (r *runtime) Module(moduleName string) api.Module {
|
|||||||
|
|
||||||
// CompileModule implements Runtime.CompileModule
|
// CompileModule implements Runtime.CompileModule
|
||||||
func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) {
|
func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) {
|
||||||
|
if err := r.failIfClosed(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if binary == nil {
|
if binary == nil {
|
||||||
return nil, errors.New("binary == nil")
|
return nil, errors.New("binary == nil")
|
||||||
}
|
}
|
||||||
@@ -203,6 +218,14 @@ func buildListeners(ctx context.Context, internal *wasm.Module) ([]experimentala
|
|||||||
return listeners, nil
|
return listeners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failIfClosed returns an error if CloseWithExitCode was called implicitly (by Close) or explicitly.
|
||||||
|
func (r *runtime) failIfClosed() error {
|
||||||
|
if closed := atomic.LoadUint64(r.closed); closed != 0 {
|
||||||
|
return fmt.Errorf("runtime closed with exit_code(%d)", uint32(closed>>32))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
|
// InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
|
||||||
func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
|
func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
|
||||||
if compiled, err := r.CompileModule(ctx, binary); err != nil {
|
if compiled, err := r.CompileModule(ctx, binary); err != nil {
|
||||||
@@ -219,6 +242,10 @@ func (r *runtime) InstantiateModule(
|
|||||||
compiled CompiledModule,
|
compiled CompiledModule,
|
||||||
mConfig ModuleConfig,
|
mConfig ModuleConfig,
|
||||||
) (mod api.Module, err error) {
|
) (mod api.Module, err error) {
|
||||||
|
if err := r.failIfClosed(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
code := compiled.(*compiledModule)
|
code := compiled.(*compiledModule)
|
||||||
config := mConfig.(*moduleConfig)
|
config := mConfig.(*moduleConfig)
|
||||||
|
|
||||||
@@ -271,7 +298,13 @@ func (r *runtime) Close(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloseWithExitCode implements Runtime.CloseWithExitCode
|
// CloseWithExitCode implements Runtime.CloseWithExitCode
|
||||||
|
//
|
||||||
|
// Note: it also marks the internal `closed` field
|
||||||
func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
|
func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
|
||||||
|
closed := uint64(1) + uint64(exitCode)<<32 // Store exitCode as high-order bits.
|
||||||
|
if !atomic.CompareAndSwapUint64(r.closed, 0, closed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err := r.store.CloseWithExitCode(ctx, exitCode)
|
err := r.store.CloseWithExitCode(ctx, exitCode)
|
||||||
if r.cache == nil {
|
if r.cache == nil {
|
||||||
// Close the engine if the cache is not configured, which means that this engine is scoped in this runtime.
|
// Close the engine if the cache is not configured, which means that this engine is scoped in this runtime.
|
||||||
|
|||||||
@@ -665,6 +665,57 @@ func TestRuntime_Close_ClosesCompiledModules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRuntime_Closed ensures invocation of closed Runtime's methods is safe.
|
||||||
|
func TestRuntime_Closed(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
errFunc func(r Runtime, mod CompiledModule) error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "InstantiateModule",
|
||||||
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
||||||
|
_, err := r.InstantiateModule(testCtx, mod, NewModuleConfig())
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InstantiateModuleFromBinary",
|
||||||
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
||||||
|
_, err := r.InstantiateModuleFromBinary(testCtx, binaryNamedZero)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CompileModule",
|
||||||
|
errFunc: func(r Runtime, mod CompiledModule) error {
|
||||||
|
_, err := r.CompileModule(testCtx, binaryNamedZero)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}}
|
||||||
|
conf := *engineLessConfig
|
||||||
|
conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine }
|
||||||
|
r := NewRuntimeWithConfig(testCtx, &conf)
|
||||||
|
defer r.Close(testCtx)
|
||||||
|
|
||||||
|
// Normally compiled modules are closed when instantiated but this is never instantiated.
|
||||||
|
mod, err := r.CompileModule(testCtx, binaryNamedZero)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), engine.CompiledModuleCount())
|
||||||
|
|
||||||
|
err = r.Close(testCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Closing the runtime should remove the compiler cache if cache is not configured.
|
||||||
|
require.True(t, engine.closed)
|
||||||
|
|
||||||
|
require.EqualError(t, tc.errFunc(r, mod), "runtime closed with exit_code(0)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type mockEngine struct {
|
type mockEngine struct {
|
||||||
name string
|
name string
|
||||||
cachedModules map[*wasm.Module]struct{}
|
cachedModules map[*wasm.Module]struct{}
|
||||||
|
|||||||
Reference in New Issue
Block a user