Exposes Module.IsClosed to prevent calling functions when closed (#1573)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-07-10 15:32:51 +08:00
committed by GitHub
parent 15fa5c4de5
commit 326c267726
6 changed files with 83 additions and 35 deletions

View File

@@ -484,26 +484,26 @@ https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/m
## Exit ## Exit
### Why do we return a `sys.ExitError` on exit code zero? ### Why do we only return a `sys.ExitError` on a non-zero exit code?
It may be surprising to find an error returned on success (exit code zero). It is reasonable to think an exit error should be returned, even if the code is
This can be explained easier when you think of function returns: When results success (zero). Even on success, the module is no longer functional. For
aren't empty, then you must return an error. This is trickier to explain when example, function exports would error later. However, wazero does not. The only
results are empty, such as the case in the "_start" function in WASI. time `sys.ExitError` is on error (non-zero).
The main rationale for returning an exit error even if the code is success is This decision was to improve performance and ergonomics for guests that both
that the module is no longer functional. For example, function exports would use WASI (have a `_start` function), and also allow custom exports.
error later. In cases like these, it is better to handle errors where they Specifically, Rust, TinyGo and normal wasi-libc, don't exit the module during
occur. `_start`. If they did, it would invalidate their function exports. This means
it is unlikely most compilers will change this behavior.
Luckily, it is not common to exit a module during the "_start" function. For `GOOS=waspi1` from Go 1.21 does exit during `_start`. However, it doesn't
example, the only known compilation target that does this is Emscripten. Most, support other exports besides `_start`, and `_start` is not defined to be
such as Rust, TinyGo, or normal wasi-libc, don't. If they did, it would called multiple times anyway.
invalidate their function exports. This means it is unlikely most compilers
will change this behavior.
In summary, we return a `sys.ExitError` to the caller whenever we get it, as it Since `sys.ExitError` is not always returned, we added `Module.IsClosed` for
properly reflects the state of the module, which would be closed on this error. defensive checks. This helps integrators avoid calling functions which will
always fail.
### Why panic with `sys.ExitError` after a host function exits? ### Why panic with `sys.ExitError` after a host function exits?

View File

@@ -150,6 +150,10 @@ type Module interface {
Memory() Memory Memory() Memory
// ExportedFunction returns a function exported from this module or nil if it wasn't. // ExportedFunction returns a function exported from this module or nil if it wasn't.
//
// Note: The default wazero.ModuleConfig attempts to invoke `_start`, which
// in rare cases can close the module. When in doubt, check IsClosed prior
// to invoking a function export after instantiation.
ExportedFunction(name string) Function ExportedFunction(name string) Function
// ExportedFunctionDefinitions returns all the exported function // ExportedFunctionDefinitions returns all the exported function
@@ -190,6 +194,21 @@ type Module interface {
// Closer closes this module by delegating to CloseWithExitCode with an exit code of zero. // Closer closes this module by delegating to CloseWithExitCode with an exit code of zero.
Closer Closer
// IsClosed returns true if the module is closed, so no longer usable.
//
// This can happen for the following reasons:
// - Closer was called directly.
// - A guest function called Closer indirectly, such as `_start` calling
// `proc_exit`, which internally closed the module.
// - wazero.RuntimeConfig `WithCloseOnContextDone` was enabled and a
// context completion closed the module.
//
// Where any of the above are possible, check this value before calling an
// ExportedFunction, even if you didn't formerly receive a sys.ExitError.
// sys.ExitError is only returned on non-zero code, something that closes
// the module successfully will not result it one.
IsClosed() bool
internalapi.WazeroOnly internalapi.WazeroOnly
} }

View File

@@ -52,14 +52,17 @@ func NewModule(memory *Memory, functions ...*Function) *Module {
return &Module{Functions: functions, ExportMemory: memory} return &Module{Functions: functions, ExportMemory: memory}
} }
// String implements fmt.Stringer.
func (m *Module) String() string { func (m *Module) String() string {
return "module[" + m.ModuleName + "]" return "module[" + m.ModuleName + "]"
} }
// Name implements the same method as documented on api.Module.
func (m *Module) Name() string { func (m *Module) Name() string {
return m.ModuleName return m.ModuleName
} }
// Memory implements the same method as documented on api.Module.
func (m *Module) Memory() api.Memory { func (m *Module) Memory() api.Memory {
if m.ExportMemory != nil { if m.ExportMemory != nil {
m.once.Do(m.initialize) m.once.Do(m.initialize)
@@ -68,16 +71,19 @@ func (m *Module) Memory() api.Memory {
return nil return nil
} }
// ExportedFunction implements the same method as documented on api.Module.
func (m *Module) ExportedFunction(name string) api.Function { func (m *Module) ExportedFunction(name string) api.Function {
m.once.Do(m.initialize) m.once.Do(m.initialize)
return m.exportedFunctions[name] return m.exportedFunctions[name]
} }
// ExportedFunctionDefinitions implements the same method as documented on api.Module.
func (m *Module) ExportedFunctionDefinitions() map[string]api.FunctionDefinition { func (m *Module) ExportedFunctionDefinitions() map[string]api.FunctionDefinition {
m.once.Do(m.initialize) m.once.Do(m.initialize)
return m.exportedFunctionDefinitions return m.exportedFunctionDefinitions
} }
// ExportedMemory implements the same method as documented on api.Module.
func (m *Module) ExportedMemory(name string) api.Memory { func (m *Module) ExportedMemory(name string) api.Memory {
if m.ExportMemory != nil && name == "memory" { if m.ExportMemory != nil && name == "memory" {
m.once.Do(m.initialize) m.once.Do(m.initialize)
@@ -86,16 +92,48 @@ func (m *Module) ExportedMemory(name string) api.Memory {
return nil return nil
} }
// ExportedMemoryDefinitions implements the same method as documented on api.Module.
func (m *Module) ExportedMemoryDefinitions() map[string]api.MemoryDefinition { func (m *Module) ExportedMemoryDefinitions() map[string]api.MemoryDefinition {
m.once.Do(m.initialize) m.once.Do(m.initialize)
return m.exportedMemoryDefinitions return m.exportedMemoryDefinitions
} }
// ExportedGlobal implements the same method as documented on api.Module.
func (m *Module) ExportedGlobal(name string) api.Global { func (m *Module) ExportedGlobal(name string) api.Global {
m.once.Do(m.initialize) m.once.Do(m.initialize)
return m.exportedGlobals[name] return m.exportedGlobals[name]
} }
// Close implements the same method as documented on api.Closer.
func (m *Module) Close(ctx context.Context) error {
return m.CloseWithExitCode(ctx, 0)
}
// CloseWithExitCode implements the same method as documented on api.Closer.
func (m *Module) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
atomic.CompareAndSwapUint64(&m.exitStatus, 0, exitStatusMarker|uint64(exitCode))
return nil
}
// IsClosed implements the same method as documented on api.Module.
func (m *Module) IsClosed() bool {
_, exited := m.ExitStatus()
return exited
}
// NumGlobal implements the same method as documented on experimental.InternalModule.
func (m *Module) NumGlobal() int {
return len(m.Globals)
}
// Global implements the same method as documented on experimental.InternalModule.
func (m *Module) Global(i int) api.Global {
m.once.Do(m.initialize)
return m.Globals[i]
}
// Below are undocumented extensions
func (m *Module) NumFunction() int { func (m *Module) NumFunction() int {
return len(m.Functions) return len(m.Functions)
} }
@@ -105,24 +143,6 @@ func (m *Module) Function(i int) api.Function {
return m.Functions[i] return m.Functions[i]
} }
func (m *Module) NumGlobal() int {
return len(m.Globals)
}
func (m *Module) Global(i int) api.Global {
m.once.Do(m.initialize)
return m.Globals[i]
}
func (m *Module) Close(ctx context.Context) error {
return m.CloseWithExitCode(ctx, 0)
}
func (m *Module) CloseWithExitCode(ctx context.Context, exitCode uint32) error {
atomic.CompareAndSwapUint64(&m.exitStatus, 0, exitStatusMarker|uint64(exitCode))
return nil
}
func (m *Module) ExitStatus() (exitCode uint32, exited bool) { func (m *Module) ExitStatus() (exitCode uint32, exited bool) {
exitStatus := atomic.LoadUint64(&m.exitStatus) exitStatus := atomic.LoadUint64(&m.exitStatus)
return uint32(exitStatus), exitStatus != 0 return uint32(exitStatus), exitStatus != 0

View File

@@ -106,6 +106,11 @@ func (m *ModuleInstance) CloseWithExitCode(ctx context.Context, exitCode uint32)
return m.ensureResourcesClosed(ctx) return m.ensureResourcesClosed(ctx)
} }
// IsClosed implements the same method as documented on api.Module.
func (m *ModuleInstance) IsClosed() bool {
return atomic.LoadUint64(&m.Closed) != 0
}
func (m *ModuleInstance) closeWithExitCodeWithoutClosingResource(exitCode uint32) (err error) { func (m *ModuleInstance) closeWithExitCodeWithoutClosingResource(exitCode uint32) (err error) {
if !m.setExitCode(exitCode, exitCodeFlagResourceNotClosed) { if !m.setExitCode(exitCode, exitCodeFlagResourceNotClosed) {
return nil // not an error to have already closed return nil // not an error to have already closed

View File

@@ -100,6 +100,9 @@ func TestModuleInstance_Close(t *testing.T) {
require.Equal(t, tc.expectedClosed, m.Closed) require.Equal(t, tc.expectedClosed, m.Closed)
// Outside callers should be able to know it was closed.
require.True(t, m.IsClosed())
// Verify our intended side-effect // Verify our intended side-effect
require.Nil(t, s.Module(moduleName)) require.Nil(t, s.Module(moduleName))

View File

@@ -320,7 +320,8 @@ func (r *runtime) InstantiateModule(
return return
} }
// Attach the code closer so that anything afterwards closes the compiled code when closing the module. // Attach the code closer so that anything afterward closes the compiled
// code when closing the module.
if code.closeWithModule { if code.closeWithModule {
mod.(*wasm.ModuleInstance).CodeCloser = code mod.(*wasm.ModuleInstance).CodeCloser = code
} }