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:
Edoardo Vacchi
2023-02-08 16:45:52 +01:00
committed by GitHub
parent 9dbed753f5
commit 39983c4080
2 changed files with 84 additions and 0 deletions

View File

@@ -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.

View File

@@ -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{}