wasi: stops enforcing _start function export (#409)

Currently, we have custom code in wapc-go because our library forces a
failure when a module that uses WASI doesn't define a "_start" function.
Using the same pragmatism that resulted in us not enforcing the WASI
table, this makes the "_start" function optional. This doesn't add a
flag as the spec is not a proper version anyway (snapshot-01), so
there's no need to further complicate configuration.

If a "_start" function exists, we enforce it is of the proper signature
and succeeds. Otherwise, we allow it to be absent.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-03-25 07:34:30 +08:00
committed by GitHub
parent 000cbdeb54
commit a1dc1f56a0
4 changed files with 65 additions and 24 deletions

View File

@@ -87,7 +87,22 @@ runtime vs interpreting Wasm directly (the `naivevm` interpreter).
Note: `microwasm` was never specified formally, and only exists in a historical codebase of wasmtime:
https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs
## Why is `SysConfig` decoupled from WASI?
## WASI
### Why aren't all WASI rules enforced?
The [snapshot-01](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) version of WASI has a
number of rules for a "command module", but only the memory export rule is enforced. If a "_start" function exists, it
is enforced to be the correct signature and succeed, but the export itself isn't enforced. It follows that this means
exports are not required to be contained to a "_start" function invocation. Finally, the "__indirect_function_table"
export is also not enforced.
The reason for the exceptions are that implementations aren't following the rules. For example, TinyGo doesn't export
"__indirect_function_table", so crashing on this would make wazero unable to run TinyGo modules. Similarly, modules
loaded by wapc-go don't always define a "_start" function. Since "snapshot-01" is not a proper version, and certainly
not a W3C recommendation, there's no sense in breaking users over matters like this.
### Why is `SysConfig` decoupled from WASI?
WebAssembly System Interfaces (WASI) is a formalization of a practice that can be done anyway: Define a host function to
access a system interface, such as writing to STDOUT. WASI stalled at snapshot-01 and as of early 2022, is being

View File

@@ -1505,9 +1505,9 @@ func openFileEntry(rootFS fs.FS, pathName string) (*internalwasm.FileEntry, wasi
}
func ValidateWASICommand(module *internalwasm.Module, moduleName string) error {
if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err != nil {
return err
} else {
// The snapshot-01 specification requires a "_start" function, but we don't enforce it is present. We only enforce
// if present, it is valid. In practice, not all modules importing WASI define a "_start" function (ex. wapc-go).
if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err == nil {
// TODO: this should be verified during decode so that errors have the correct source positions
ft := module.TypeOfFunction(start.Index)
if ft == nil {

34
wasi.go
View File

@@ -40,9 +40,9 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) {
}
}
// StartWASICommand instantiates the module and starts its WASI Command function ("_start"). The return value are all
// exported functions in the module. This errs if the module doesn't comply with prerequisites, or any instantiation
// or function call error.
// StartWASICommand instantiates the module and starts its WASI Command function ("_start") if present. The return value
// are all exported functions in the module. This errs if the module doesn't export a memory named "memory", or there
// are any instantiation or function call errors. On success, other modules can import wasi.ModuleSnapshotPreview1.
//
// Ex.
// r := wazero.NewRuntime()
@@ -53,13 +53,17 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) {
// module, _ := StartWASICommand(r, decoded)
// defer module.Close()
//
// Prerequisites of the "Current Unstable ABI" from wasi.ModuleSnapshotPreview1:
// * "_start" is an exported nullary function and does not export "_initialize"
// * "memory" is an exported memory.
// ## "memory" export
// WASI snapshot-01 requires exporting a memory named "memory", and wazero enforces this as nearly all functions use
// memory to implement multiple returns. StartWASICommand errs if there is no memory exported as "memory".
//
// ## "_start" function export
// WASI snapshot-01 requires exporting a function named "_start" for WASI command, but wazero does not enforce this. If it is defined,
// it is called directly after any module-defined start function, in the runtime context (RuntimeConfig.WithContext).
//
// ## "__indirect_function_table" function export
// WASI snapshot-01 requires exporting a table named "__indirect_function_table", but wazero does not enforce this.
//
// Note: "_start" is invoked in the RuntimeConfig.Context.
// Note: Exporting "__indirect_function_table" is mentioned as required, but not enforced here.
// Note: The wasm.Functions return value does not restrict exports after "_start" as allowed in the specification.
// Note: All TinyGo Wasm are WASI commands. They initialize memory on "_start" and import "fd_write" to implement panic.
// See StartWASICommandWithConfig
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
@@ -97,7 +101,8 @@ func startWASICommandWithSysContext(r Runtime, module *Module, sys *internalwasm
internal, ok := r.(*runtime)
if !ok {
return nil, fmt.Errorf("unsupported Runtime implementation: %s", r)
err = fmt.Errorf("unsupported Runtime implementation: %s", r)
return
}
if mod, err = internal.store.Instantiate(internal.ctx, module.module, module.name, sys); err != nil {
@@ -105,8 +110,11 @@ func startWASICommandWithSysContext(r Runtime, module *Module, sys *internalwasm
}
start := mod.ExportedFunction(internalwasi.FunctionStart)
if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil {
return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
if start == nil {
return
}
return mod, nil
if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil {
err = fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
}
return
}

View File

@@ -11,7 +11,24 @@ import (
"github.com/tetratelabs/wazero/wasm"
)
func TestStartWASICommand_UsesStoreContext(t *testing.T) {
// TestStartWASICommand_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't export "_start".
func TestStartWASICommand_DoesntEnforce_Start(t *testing.T) {
r := NewRuntime()
wasi, err := r.InstantiateModule(WASISnapshotPreview1())
require.NoError(t, err)
defer wasi.Close()
// Start the module as a WASI command. This will fail if the context wasn't as intended.
mod, err := StartWASICommandFromSource(r, []byte(`(module $wasi_test.go
(memory 1)
(export "memory" (memory 0))
)`))
require.NoError(t, err)
require.NoError(t, mod.Close())
}
func TestStartWASICommand_UsesRuntimeContext(t *testing.T) {
type key string
config := NewRuntimeConfig().WithContext(context.WithValue(context.Background(), key("wa"), "zero"))
r := NewRuntimeWithConfig(config)
@@ -23,23 +40,24 @@ func TestStartWASICommand_UsesStoreContext(t *testing.T) {
require.Equal(t, config.ctx, ctx.Context())
}
_, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
host, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
require.NoError(t, err)
defer host.Close()
_, err = r.InstantiateModule(WASISnapshotPreview1())
wasi, err := r.InstantiateModule(WASISnapshotPreview1())
require.NoError(t, err)
defer wasi.Close()
decoded, err := r.CompileModule([]byte(`(module $wasi_test.go
// Start the module as a WASI command. This will fail if the context wasn't as intended.
mod, err := StartWASICommandFromSource(r, []byte(`(module $wasi_test.go
(import "" "start" (func $start))
(memory 1)
(export "_start" (func $start))
(export "memory" (memory 0))
)`))
require.NoError(t, err)
defer mod.Close()
// Start the module as a WASI command. This will fail if the context wasn't as intended.
_, err = StartWASICommand(r, decoded)
require.NoError(t, err)
require.True(t, calledStart)
}