Adds Runtime.WithCapacityPages to avoid allocations during runtime. (#514)

`Runtime.WithMemoryCapacityPages` is a function that determines memory
capacity in pages (65536 bytes per page). The inputs are the min and
possibly nil max defined by the module, and the default is to return
the min.

Ex. To set capacity to max when exists:
```golang
c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
	if maxPages != nil {
		return *maxPages
	}
	return minPages
})
```

Note: This applies at compile time, ModuleBuilder.Build or Runtime.CompileModule.

Fixes #500

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-29 17:54:48 +08:00
committed by GitHub
parent 189b694140
commit 2c03098dba
30 changed files with 532 additions and 217 deletions

View File

@@ -30,8 +30,10 @@ import (
// env2, _ := r.InstantiateModuleWithConfig(ctx, env, NewModuleConfig().WithName("env.2"))
// defer env2.Close(ctx)
//
// Note: Builder methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
// Note: Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
// Notes:
// * ModuleBuilder is mutable. WithXXX functions return the same instance for chaining.
// * WithXXX methods do not return errors, to allow chaining. Any validation errors are deferred until Build.
// * Insertion order is not retained. Anything defined by this builder is sorted lexicographically on Build.
type ModuleBuilder interface {
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
@@ -91,7 +93,7 @@ type ModuleBuilder interface {
// // (memory (export "memory") 1)
// builder.ExportMemory(1)
//
// Note: This is allowed to grow to RuntimeConfig.WithMemoryMaxPages (4GiB). To bound it, use ExportMemoryWithMax.
// Note: This is allowed to grow to RuntimeConfig.WithMemoryLimitPages (4GiB). To bound it, use ExportMemoryWithMax.
// Note: If a memory is already exported with the same name, this overwrites it.
// Note: Version 1.0 (20191205) of the WebAssembly spec allows at most one memory per module.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
@@ -103,7 +105,7 @@ type ModuleBuilder interface {
// // (memory (export "memory") 1 1)
// builder.ExportMemoryWithMax(1, 1)
//
// Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryMaxPages
// Note: maxPages must be at least minPages and no larger than RuntimeConfig.WithMemoryLimitPages
ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder
// ExportGlobalI32 exports a global constant of type api.ValueTypeI32.
@@ -195,13 +197,17 @@ func (b *moduleBuilder) ExportFunctions(nameToGoFunc map[string]interface{}) Mod
// ExportMemory implements ModuleBuilder.ExportMemory
func (b *moduleBuilder) ExportMemory(name string, minPages uint32) ModuleBuilder {
b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: b.r.memoryMaxPages}
mem := &wasm.Memory{Min: minPages, Max: b.r.memoryLimitPages}
mem.Cap = b.r.memoryCapacityPages(mem.Min, nil)
b.nameToMemory[name] = mem
return b
}
// ExportMemoryWithMax implements ModuleBuilder.ExportMemoryWithMax
func (b *moduleBuilder) ExportMemoryWithMax(name string, minPages, maxPages uint32) ModuleBuilder {
b.nameToMemory[name] = &wasm.Memory{Min: minPages, Max: maxPages}
mem := &wasm.Memory{Min: minPages, Max: maxPages, IsMaxEncoded: true}
mem.Cap = b.r.memoryCapacityPages(mem.Min, &maxPages)
b.nameToMemory[name] = mem
return b
}
@@ -246,11 +252,13 @@ func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder {
// Build implements ModuleBuilder.Build
func (b *moduleBuilder) Build(ctx context.Context) (*CompiledCode, error) {
// Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule
maxLimit := b.r.memoryMaxPages
memoryLimitPages := b.r.memoryLimitPages
for name, mem := range b.nameToMemory {
if mem.Max > maxLimit {
max := mem.Max
return nil, fmt.Errorf("memory[%s] max %d pages (%s) outside range of %d pages (%s)", name, max, wasm.PagesToUnitOfBytes(max), maxLimit, wasm.PagesToUnitOfBytes(maxLimit))
if err := mem.ValidateMinMax(memoryLimitPages); err != nil {
return nil, fmt.Errorf("memory[%s] %v", name, err)
}
if err := b.r.setMemoryCapacity(name, mem); err != nil {
return nil, err
}
}

View File

@@ -159,7 +159,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
return r.NewModuleBuilder("").ExportMemory("memory", 1)
},
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: wasm.MemoryLimitPages},
ExportSection: []*wasm.Export{
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -171,7 +171,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
return r.NewModuleBuilder("").ExportMemory("memory", 1).ExportMemory("memory", 2)
},
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 2, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: wasm.MemoryLimitPages},
ExportSection: []*wasm.Export{
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -183,7 +183,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1)
},
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: 1},
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 1, IsMaxEncoded: true},
ExportSection: []*wasm.Export{
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -195,7 +195,7 @@ func TestNewModuleBuilder_Build(t *testing.T) {
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, 1).ExportMemoryWithMax("memory", 1, 2)
},
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: 2},
MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 2, IsMaxEncoded: true},
ExportSection: []*wasm.Export{
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -362,22 +362,26 @@ func TestNewModuleBuilder_Build(t *testing.T) {
func TestNewModuleBuilder_Build_Errors(t *testing.T) {
tests := []struct {
name string
input func(Runtime) ModuleBuilder
input func(*RuntimeConfig) ModuleBuilder
expectedErr string
}{
{
name: "memory max > limit",
input: func(r Runtime) ModuleBuilder {
return r.NewModuleBuilder("").ExportMemory("memory", math.MaxUint32)
name: "memory min > limit", // only one test to avoid duplicating tests in module_test.go
input: func(cfg *RuntimeConfig) ModuleBuilder {
return NewRuntimeWithConfig(cfg).NewModuleBuilder("").
ExportMemory("memory", math.MaxUint32)
},
expectedErr: "memory[memory] min 4294967295 pages (3 Ti) > max 65536 pages (4 Gi)",
expectedErr: "memory[memory] min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
},
{
name: "memory min > limit",
input: func(r Runtime) ModuleBuilder {
return r.NewModuleBuilder("").ExportMemoryWithMax("memory", 1, math.MaxUint32)
name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go
input: func(cfg *RuntimeConfig) ModuleBuilder {
cfg = cfg.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
return 1
})
return NewRuntimeWithConfig(cfg).NewModuleBuilder("").ExportMemory("memory", 2)
},
expectedErr: "memory[memory] max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
},
}
@@ -385,7 +389,7 @@ func TestNewModuleBuilder_Build_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, e := tc.input(NewRuntime()).Build(testCtx)
_, e := tc.input(NewRuntimeConfig()).Build(testCtx)
require.EqualError(t, e, tc.expectedErr)
})
}

View File

@@ -15,24 +15,29 @@ import (
)
// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig
//
// Note: RuntimeConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
type RuntimeConfig struct {
enabledFeatures wasm.Features
newEngine func(wasm.Features) wasm.Engine
memoryMaxPages uint32
enabledFeatures wasm.Features
newEngine func(wasm.Features) wasm.Engine
memoryLimitPages uint32
memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32
}
// engineLessConfig helps avoid copy/pasting the wrong defaults.
var engineLessConfig = &RuntimeConfig{
enabledFeatures: wasm.Features20191205,
memoryMaxPages: wasm.MemoryMaxPages,
enabledFeatures: wasm.Features20191205,
memoryLimitPages: wasm.MemoryLimitPages,
memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 { return minPages },
}
// clone ensures all fields are coped even if nil.
// clone ensures all fields are copied even if nil.
func (c *RuntimeConfig) clone() *RuntimeConfig {
return &RuntimeConfig{
enabledFeatures: c.enabledFeatures,
newEngine: c.newEngine,
memoryMaxPages: c.memoryMaxPages,
enabledFeatures: c.enabledFeatures,
newEngine: c.newEngine,
memoryLimitPages: c.memoryLimitPages,
memoryCapacityPages: c.memoryCapacityPages,
}
}
@@ -53,19 +58,41 @@ func NewRuntimeConfigInterpreter() *RuntimeConfig {
return ret
}
// WithMemoryMaxPages reduces the maximum number of pages a module can define from 65536 pages (4GiB) to a lower value.
// WithMemoryLimitPages limits the maximum number of pages a module can define from 65536 pages (4GiB) to a lower value.
//
// Notes:
// * If a module defines no memory max limit, Runtime.CompileModule sets max to this value.
// * If a module defines a memory max larger than this amount, it will fail to compile (Runtime.CompileModule).
// * If a module defines no memory max value, Runtime.CompileModule sets max to the limit.
// * If a module defines a memory max larger than this limit, it will fail to compile (Runtime.CompileModule).
// * Any "memory.grow" instruction that results in a larger value than this results in an error at runtime.
// * Zero is a valid value and results in a crash if any module uses memory.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-types%E2%91%A0
func (c *RuntimeConfig) WithMemoryMaxPages(memoryMaxPages uint32) *RuntimeConfig {
func (c *RuntimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) *RuntimeConfig {
ret := c.clone()
ret.memoryMaxPages = memoryMaxPages
ret.memoryLimitPages = memoryLimitPages
return ret
}
// WithMemoryCapacityPages is a function that determines memory capacity in pages (65536 bytes per page). The inputs are
// the min and possibly nil max defined by the module, and the default is to return the min.
//
// Ex. To set capacity to max when exists:
// c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
// if maxPages != nil {
// return *maxPages
// }
// return minPages
// })
//
// This function is used at compile time (ModuleBuilder.Build or Runtime.CompileModule). Compile will err if the
// function returns a value lower than minPages or greater than WithMemoryLimitPages.
func (c *RuntimeConfig) WithMemoryCapacityPages(maxCapacityPages func(minPages uint32, maxPages *uint32) uint32) *RuntimeConfig {
if maxCapacityPages == nil {
return c // Instead of erring.
}
ret := c.clone()
ret.memoryCapacityPages = maxCapacityPages
return ret
}
@@ -183,12 +210,15 @@ func (c *CompiledCode) Close(_ context.Context) error {
return nil
}
// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating system.
// Using this, resources such as STDIN can be isolated (ex via StartWASICommandWithConfig), so that the same module can
// be safely instantiated multiple times.
// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
// system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated
// multiple times.
//
// Note: While wazero supports Windows as a platform, host functions using ModuleConfig follow a UNIX dialect.
// See RATIONALE.md for design background and relationship to WebAssembly System Interfaces (WASI).
//
// TODO: This is accidentally mutable. A follow-up PR should change it to be immutable as that's how baseline
// configuration can be used safely in modules instantiated on different goroutines.
type ModuleConfig struct {
name string
startFunctions []string

View File

@@ -18,12 +18,12 @@ func TestRuntimeConfig(t *testing.T) {
expected *RuntimeConfig
}{
{
name: "WithMemoryMaxPages",
name: "WithMemoryLimitPages",
with: func(c *RuntimeConfig) *RuntimeConfig {
return c.WithMemoryMaxPages(1)
return c.WithMemoryLimitPages(1)
},
expected: &RuntimeConfig{
memoryMaxPages: 1,
memoryLimitPages: 1,
},
},
{
@@ -83,6 +83,24 @@ func TestRuntimeConfig(t *testing.T) {
require.Equal(t, &RuntimeConfig{}, input)
})
}
t.Run("WithMemoryCapacityPages", func(t *testing.T) {
c := NewRuntimeConfig()
// Test default returns min
require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil))
// Nil ignored
c = c.WithMemoryCapacityPages(nil)
require.Equal(t, uint32(1), c.memoryCapacityPages(1, nil))
// Assign a valid function
c = c.WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 {
return 2
})
// Returns updated value
require.Equal(t, uint32(2), c.memoryCapacityPages(1, nil))
})
}
func TestRuntimeConfig_FeatureToggle(t *testing.T) {

View File

@@ -68,7 +68,7 @@ func TestBinaryEncoder(t *testing.T) {
buf = requireStripCustomSections(t, buf)
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryMaxPages)
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryLimitPages)
require.NoError(t, err)
encodedBuf := binary.EncodeModule(mod)

View File

@@ -238,7 +238,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
(func (param f64 f64) local.get 0 drop local.get 1 drop)
(export "print_f64_f64" (func 6))
)`), wasm.Features20191205, wasm.MemoryMaxPages)
)`), wasm.Features20191205, wasm.MemoryLimitPages)
require.NoError(t, err)
// (global (export "global_i32") i32 (i32.const 666))
@@ -267,6 +267,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
mod.TableSection = &wasm.Table{Min: 10, Max: &tableLimitMax}
mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable})
maybeSetMemoryCap(mod)
err = store.Engine.CompileModule(testCtx, mod)
require.NoError(t, err)
@@ -274,7 +275,14 @@ func addSpectestModule(t *testing.T, store *wasm.Store) {
require.NoError(t, err)
}
// Run runs all the test inside of the testDataFS file system where all the cases are descirbed
// maybeSetMemoryCap assigns wasm.Memory Cap to Min, which is what wazero.CompileModule would do.
func maybeSetMemoryCap(mod *wasm.Module) {
if mem := mod.MemorySection; mem != nil {
mem.Cap = mem.Min
}
}
// Run runs all the test inside the testDataFS file system where all the cases are described
// via JSON files created from wast2json.
func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.Engine, enabledFeatures wasm.Features) {
files, err := testDataFS.ReadDir("testdata")
@@ -313,7 +321,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
case "module":
buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemoryMaxPages)
mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages)
require.NoError(t, err, msg)
require.NoError(t, mod.Validate(enabledFeatures))
mod.AssignModuleID(buf)
@@ -329,6 +337,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
}
}
maybeSetMemoryCap(mod)
err = store.Engine.CompileModule(testCtx, mod)
require.NoError(t, err, msg)
@@ -446,7 +455,7 @@ func Run(t *testing.T, testDataFS embed.FS, newEngine func(wasm.Features) wasm.E
}
func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg string) {
mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryMaxPages)
mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryLimitPages)
if err != nil {
return
}
@@ -458,6 +467,7 @@ func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg
mod.AssignModuleID(buf)
maybeSetMemoryCap(mod)
err = store.Engine.CompileModule(testCtx, mod)
if err != nil {
return

View File

@@ -92,7 +92,7 @@ func newExample() *wasm.Module {
func BenchmarkWat2Wasm(b *testing.B, vsName string, vsWat2Wasm func([]byte) error) {
b.Run("wazero", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
if m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages); err != nil {
b.Fatal(err)
} else {
_ = binary.EncodeModule(m)

View File

@@ -14,13 +14,13 @@ import (
func TestExampleUpToDate(t *testing.T) {
t.Run("binary.DecodeModule", func(t *testing.T) {
m, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryMaxPages)
m, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryLimitPages)
require.NoError(t, err)
require.Equal(t, example, m)
})
t.Run("text.DecodeModule", func(t *testing.T) {
m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages)
m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages)
require.NoError(t, err)
require.Equal(t, example, m)
})
@@ -49,7 +49,7 @@ func BenchmarkCodec(b *testing.B) {
b.Run("binary.DecodeModule", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
if _, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryLimitPages); err != nil {
b.Fatal(err)
}
}
@@ -63,7 +63,7 @@ func BenchmarkCodec(b *testing.B) {
b.Run("text.DecodeModule", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
if _, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryLimitPages); err != nil {
b.Fatal(err)
}
}

View File

@@ -166,7 +166,7 @@ func (g *generator) genImportSection() {
if memoryImported == 0 {
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
max := g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min
max := g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min
imp.Type = wasm.ExternTypeMemory
imp.DescMem = &wasm.Memory{
@@ -180,7 +180,7 @@ func (g *generator) genImportSection() {
if tableImported == 0 {
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
max := uint32(g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min)
max := uint32(g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min)
imp.Type = wasm.ExternTypeTable
tableImported = 1
@@ -215,7 +215,7 @@ func (g *generator) genTableSection() {
}
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
max := uint32(g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min)
max := uint32(g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min)
g.m.TableSection = &wasm.Table{Min: uint32(min), Max: &max}
}
@@ -225,7 +225,7 @@ func (g *generator) genMemorySection() {
return
}
min := g.nextRandom().Intn(4) // Min in reality is relatively small like 4.
max := g.nextRandom().Intn(int(wasm.MemoryMaxPages)-min) + min
max := g.nextRandom().Intn(int(wasm.MemoryLimitPages)-min) + min
g.m.MemorySection = &wasm.Memory{Min: uint32(min), Max: uint32(max), IsMaxEncoded: true}
}
@@ -412,7 +412,7 @@ func (g *generator) newCode() *wasm.Code {
wasm.OpcodeEnd}}
}
// genDataSection generates random data section if memory is declared and its minums is not zero.
// genDataSection generates random data section if memory is declared and its min is not zero.
func (g *generator) genDataSection() {
_, _, mem, _, err := g.m.AllDeclarations()
if err != nil {

View File

@@ -703,15 +703,15 @@ func TestGenerator_dataSection(t *testing.T) {
{
numData: 1,
ints: []int{
int(wasm.MemoryMaxPages) - 1, // offset
1, // size of inits
int(wasm.MemoryLimitPages) - 1, // offset
1, // size of inits
},
bufs: [][]byte{{0x1}},
exps: []*wasm.DataSegment{
{
OffsetExpression: &wasm.ConstantExpression{
Opcode: wasm.OpcodeI32Const,
Data: leb128.EncodeUint32(uint32(wasm.MemoryMaxPages) - 1),
Data: leb128.EncodeUint32(uint32(wasm.MemoryLimitPages) - 1),
},
Init: []byte{0x1},
},

View File

@@ -11,7 +11,7 @@ import (
// DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Binary Format
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-format%E2%91%A0
func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages uint32) (*wasm.Module, error) {
func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryLimitPages uint32) (*wasm.Module, error) {
r := bytes.NewReader(binary)
// Magic number.
@@ -71,7 +71,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages u
case wasm.SectionIDType:
m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
case wasm.SectionIDImport:
if m.ImportSection, err = decodeImportSection(r, memoryMaxPages); err != nil {
if m.ImportSection, err = decodeImportSection(r, memoryLimitPages); err != nil {
return nil, err // avoid re-wrapping the error.
}
case wasm.SectionIDFunction:
@@ -79,7 +79,7 @@ func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages u
case wasm.SectionIDTable:
m.TableSection, err = decodeTableSection(r)
case wasm.SectionIDMemory:
m.MemorySection, err = decodeMemorySection(r, memoryMaxPages)
m.MemorySection, err = decodeMemorySection(r, memoryLimitPages)
case wasm.SectionIDGlobal:
if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil {
return nil, err // avoid re-wrapping the error.

View File

@@ -80,7 +80,7 @@ func TestDecodeModule(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemoryMaxPages)
m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemoryLimitPages)
require.NoError(t, e)
require.Equal(t, tc.input, m)
})
@@ -91,7 +91,7 @@ func TestDecodeModule(t *testing.T) {
wasm.SectionIDCustom, 0xf, // 15 bytes in this section
0x04, 'm', 'e', 'm', 'e',
1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages)
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
require.NoError(t, e)
require.Equal(t, &wasm.Module{}, m)
})
@@ -106,24 +106,24 @@ func TestDecodeModule(t *testing.T) {
subsectionIDModuleName, 0x07, // 7 bytes in this subsection
0x06, // the Module name simple is 6 bytes long
's', 'i', 'm', 'p', 'l', 'e')
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages)
m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
require.NoError(t, e)
require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m)
})
t.Run("data count section disabled", func(t *testing.T) {
input := append(append(Magic, version...),
wasm.SectionIDDataCount, 1, 0)
_, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages)
_, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryLimitPages)
require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`)
})
}
func TestDecodeModule_Errors(t *testing.T) {
tests := []struct {
name string
input []byte
memoryMaxPages uint32
expectedErr string
name string
input []byte
memoryLimitPages uint32
expectedErr string
}{
{
name: "wrong magic",
@@ -150,12 +150,12 @@ func TestDecodeModule_Errors(t *testing.T) {
for _, tt := range tests {
tc := tt
if tc.memoryMaxPages == 0 {
tc.memoryMaxPages = wasm.MemoryMaxPages
if tc.memoryLimitPages == 0 {
tc.memoryLimitPages = wasm.MemoryLimitPages
}
t.Run(tc.name, func(t *testing.T) {
_, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryMaxPages)
_, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryLimitPages)
require.EqualError(t, e, tc.expectedErr)
})
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
func decodeImport(r *bytes.Reader, idx uint32, memoryMaxPages uint32) (i *wasm.Import, err error) {
func decodeImport(r *bytes.Reader, idx uint32, memoryLimitPages uint32) (i *wasm.Import, err error) {
i = &wasm.Import{}
if i.Module, _, err = decodeUTF8(r, "import module"); err != nil {
return nil, fmt.Errorf("import[%d] error decoding module: %w", idx, err)
@@ -29,7 +29,7 @@ func decodeImport(r *bytes.Reader, idx uint32, memoryMaxPages uint32) (i *wasm.I
case wasm.ExternTypeTable:
i.DescTable, err = decodeTable(r)
case wasm.ExternTypeMemory:
i.DescMem, err = decodeMemory(r, memoryMaxPages)
i.DescMem, err = decodeMemory(r, memoryLimitPages)
case wasm.ExternTypeGlobal:
i.DescGlobal, err = decodeGlobalType(r)
default:

View File

@@ -139,7 +139,7 @@ func TestEncodeImport(t *testing.T) {
Type: wasm.ExternTypeMemory,
Module: "my",
Name: "memory",
DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages, IsMaxEncoded: false},
DescMem: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages, IsMaxEncoded: false},
},
expected: []byte{
0x02, 'm', 'y',

View File

@@ -2,7 +2,6 @@ package binary
import (
"bytes"
"fmt"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -10,7 +9,7 @@ import (
// decodeMemory returns the api.Memory decoded with the WebAssembly 1.0 (20191205) Binary Format.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory
func decodeMemory(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error) {
func decodeMemory(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) {
min, maxP, err := decodeLimitsType(r)
if err != nil {
return nil, err
@@ -21,17 +20,9 @@ func decodeMemory(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error)
if maxP != nil {
isMaxEncoded = true
max = *maxP
} else {
max = memoryMaxPages
}
if max > memoryMaxPages {
return nil, fmt.Errorf("max %d pages (%s) outside range of %d pages (%s)", max, wasm.PagesToUnitOfBytes(max), memoryMaxPages, wasm.PagesToUnitOfBytes(memoryMaxPages))
} else if min > memoryMaxPages {
return nil, fmt.Errorf("min %d pages (%s) outside range of %d pages (%s)", min, wasm.PagesToUnitOfBytes(min), memoryMaxPages, wasm.PagesToUnitOfBytes(memoryMaxPages))
} else if min > max {
return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", min, wasm.PagesToUnitOfBytes(min), max, wasm.PagesToUnitOfBytes(max))
}
return &wasm.Memory{Min: min, Max: max, IsMaxEncoded: isMaxEncoded}, nil
mem := &wasm.Memory{Min: min, Max: max, IsMaxEncoded: isMaxEncoded}
return mem, mem.ValidateMinMax(memoryLimitPages)
}
// encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format.

View File

@@ -11,7 +11,7 @@ import (
func TestMemoryType(t *testing.T) {
zero := uint32(0)
max := wasm.MemoryMaxPages
max := wasm.MemoryLimitPages
tests := []struct {
name string
@@ -20,12 +20,12 @@ func TestMemoryType(t *testing.T) {
}{
{
name: "min 0",
input: &wasm.Memory{Max: wasm.MemoryMaxPages, IsMaxEncoded: true},
input: &wasm.Memory{Max: wasm.MemoryLimitPages, IsMaxEncoded: true},
expected: []byte{0x1, 0, 0x80, 0x80, 0x4},
},
{
name: "min 0 - default max",
input: &wasm.Memory{Max: wasm.MemoryMaxPages},
input: &wasm.Memory{Max: wasm.MemoryLimitPages},
expected: []byte{0x0, 0},
},
{
@@ -68,10 +68,10 @@ func TestMemoryType(t *testing.T) {
func TestDecodeMemoryType_Errors(t *testing.T) {
tests := []struct {
name string
input []byte
memoryMaxPages uint32
expectedErr string
name string
input []byte
memoryLimitPages uint32
expectedErr string
}{
{
name: "max < min",
@@ -81,24 +81,24 @@ func TestDecodeMemoryType_Errors(t *testing.T) {
{
name: "min > limit",
input: []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xf},
expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
},
{
name: "max > limit",
input: []byte{0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf},
expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
},
}
for _, tt := range tests {
tc := tt
if tc.memoryMaxPages == 0 {
tc.memoryMaxPages = wasm.MemoryMaxPages
if tc.memoryLimitPages == 0 {
tc.memoryLimitPages = wasm.MemoryLimitPages
}
t.Run(tc.name, func(t *testing.T) {
_, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryMaxPages)
_, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryLimitPages)
require.EqualError(t, err, tc.expectedErr)
})
}

View File

@@ -24,7 +24,7 @@ func decodeTypeSection(enabledFeatures wasm.Features, r *bytes.Reader) ([]*wasm.
return result, nil
}
func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import, error) {
func decodeImportSection(r *bytes.Reader, memoryLimitPages uint32) ([]*wasm.Import, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("get size of vector: %w", err)
@@ -32,7 +32,7 @@ func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import
result := make([]*wasm.Import, vs)
for i := uint32(0); i < vs; i++ {
if result[i], err = decodeImport(r, i, memoryMaxPages); err != nil {
if result[i], err = decodeImport(r, i, memoryLimitPages); err != nil {
return nil, err
}
}
@@ -66,7 +66,7 @@ func decodeTableSection(r *bytes.Reader) (*wasm.Table, error) {
return decodeTable(r)
}
func decodeMemorySection(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error) {
func decodeMemorySection(r *bytes.Reader, memoryLimitPages uint32) (*wasm.Memory, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("error reading size")
@@ -75,7 +75,7 @@ func decodeMemorySection(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory,
return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs)
}
return decodeMemory(r, memoryMaxPages)
return decodeMemory(r, memoryLimitPages)
}
func decodeGlobalSection(r *bytes.Reader, enabledFeatures wasm.Features) ([]*wasm.Global, error) {

View File

@@ -84,7 +84,7 @@ func TestMemorySection(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryMaxPages)
memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryLimitPages)
require.NoError(t, err)
require.Equal(t, tc.expected, memories)
})
@@ -93,10 +93,10 @@ func TestMemorySection(t *testing.T) {
func TestMemorySection_Errors(t *testing.T) {
tests := []struct {
name string
input []byte
memoryMaxPages uint32
expectedErr string
name string
input []byte
memoryLimitPages uint32
expectedErr string
}{
{
name: "min and min with max",
@@ -112,12 +112,12 @@ func TestMemorySection_Errors(t *testing.T) {
for _, tt := range tests {
tc := tt
if tc.memoryMaxPages == 0 {
tc.memoryMaxPages = wasm.MemoryMaxPages
if tc.memoryLimitPages == 0 {
tc.memoryLimitPages = wasm.MemoryLimitPages
}
t.Run(tc.name, func(t *testing.T) {
_, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryMaxPages)
_, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryLimitPages)
require.EqualError(t, err, tc.expectedErr)
})
}

View File

@@ -6,6 +6,8 @@ import (
"encoding/binary"
"fmt"
"math"
"reflect"
"unsafe"
"github.com/tetratelabs/wazero/api"
)
@@ -15,9 +17,9 @@ const (
// and is defined as 2^16 = 65536.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0
MemoryPageSize = uint32(65536)
// MemoryMaxPages is maximum number of pages defined (2^16).
// MemoryLimitPages is maximum number of pages defined (2^16).
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
MemoryMaxPages = uint32(65536)
MemoryLimitPages = uint32(65536)
// MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize".
MemoryPageSizeInBits = 16
)
@@ -31,8 +33,8 @@ var _ api.Memory = &MemoryInstance{}
// wasm.Store Memories index zero: `store.Memories[0]`
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
type MemoryInstance struct {
Buffer []byte
Min, Max uint32
Buffer []byte
Min, Cap, Max uint32
}
// Size implements the same method as documented on api.Memory.
@@ -193,17 +195,25 @@ func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
//
// Returns -1 if the operation resulted in exceeding the maximum memory pages.
// Otherwise, returns the prior memory size after growing the memory buffer.
func (m *MemoryInstance) Grow(_ context.Context, newPages uint32) (result uint32) {
func (m *MemoryInstance) Grow(_ context.Context, delta uint32) (result uint32) {
// Note: If you use the context.Context param, don't forget to coerce nil to context.Background()!
currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
if delta == 0 {
return currentPages
}
// If exceeds the max of memory size, we push -1 according to the spec.
if currentPages+newPages > m.Max {
newPages := currentPages + delta
if newPages > m.Max {
return 0xffffffff // = -1 in signed 32-bit integer.
} else {
// Otherwise, grow the memory.
m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(newPages))...)
} else if newPages > m.Cap { // grow the memory.
m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...)
m.Cap = newPages
return currentPages
} else { // We already have the capacity we need.
sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer))
sp.Len = int(MemoryPagesToBytesNum(newPages))
return currentPages
}
}

View File

@@ -11,7 +11,7 @@ import (
func TestMemoryPageConsts(t *testing.T) {
require.Equal(t, MemoryPageSize, uint32(1)<<MemoryPageSizeInBits)
require.Equal(t, MemoryPageSize, uint32(1<<16))
require.Equal(t, MemoryMaxPages, uint32(1<<16))
require.Equal(t, MemoryLimitPages, uint32(1<<16))
}
func Test_MemoryPagesToBytesNum(t *testing.T) {
@@ -27,28 +27,56 @@ func Test_MemoryBytesNumToPages(t *testing.T) {
}
func TestMemoryInstance_Grow_Size(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
max := uint32(10)
m := &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
require.Equal(t, uint32(0), m.Grow(ctx, 5))
require.Equal(t, uint32(5), m.PageSize(ctx))
tests := []struct {
name string
ctx context.Context
capEqualsMax bool
}{
{name: "nil context"},
{name: "context", ctx: testCtx},
{name: "nil context, capEqualsMax", capEqualsMax: true},
{name: "context, capEqualsMax", ctx: testCtx, capEqualsMax: true},
}
// Zero page grow is well-defined, should return the current page correctly.
require.Equal(t, uint32(5), m.Grow(ctx, 0))
require.Equal(t, uint32(5), m.PageSize(ctx))
require.Equal(t, uint32(5), m.Grow(ctx, 4))
require.Equal(t, uint32(9), m.PageSize(ctx))
for _, tt := range tests {
tc := tt
// At this point, the page size equal 9,
// so trying to grow two pages should result in failure.
require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
require.Equal(t, uint32(9), m.PageSize(ctx))
t.Run(tc.name, func(t *testing.T) {
ctx := tc.ctx
max := uint32(10)
maxBytes := MemoryPagesToBytesNum(max)
var m *MemoryInstance
if tc.capEqualsMax {
m = &MemoryInstance{Cap: max, Max: max, Buffer: make([]byte, 0, maxBytes)}
} else {
m = &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
}
require.Equal(t, uint32(0), m.Grow(ctx, 5))
require.Equal(t, uint32(5), m.PageSize(ctx))
// But growing one page is still permitted.
require.Equal(t, uint32(9), m.Grow(ctx, 1))
// Zero page grow is well-defined, should return the current page correctly.
require.Equal(t, uint32(5), m.Grow(ctx, 0))
require.Equal(t, uint32(5), m.PageSize(ctx))
require.Equal(t, uint32(5), m.Grow(ctx, 4))
require.Equal(t, uint32(9), m.PageSize(ctx))
// Ensure that the current page size equals the max.
require.Equal(t, max, m.PageSize(ctx))
// At this point, the page size equal 9,
// so trying to grow two pages should result in failure.
require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
require.Equal(t, uint32(9), m.PageSize(ctx))
// But growing one page is still permitted.
require.Equal(t, uint32(9), m.Grow(ctx, 1))
// Ensure that the current page size equals the max.
require.Equal(t, max, m.PageSize(ctx))
if tc.capEqualsMax { // Ensure the capacity isn't more than max.
require.Equal(t, maxBytes, uint64(cap(m.Buffer)))
} else { // Slice doubles, so it should have a higher capacity than max.
require.True(t, maxBytes < uint64(cap(m.Buffer)))
}
})
}
}
@@ -130,7 +158,7 @@ func TestPagesToUnitOfBytes(t *testing.T) {
},
{
name: "max memory",
pages: MemoryMaxPages,
pages: MemoryLimitPages,
expected: "4 Gi",
},
{

View File

@@ -23,7 +23,7 @@ import (
// * result is the module parsed or nil on error
// * err is a FormatError invoking the parser, dangling block comments or unexpected characters.
// See binary.DecodeModule and text.DecodeModule
type DecodeModule func(source []byte, enabledFeatures Features, memoryMaxPages uint32) (result *Module, err error)
type DecodeModule func(source []byte, enabledFeatures Features, memoryLimitPages uint32) (result *Module, err error)
// EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
// See binary.EncodeModule
@@ -559,9 +559,12 @@ func paramNames(localNames IndirectNameMap, funcIdx uint32, paramLen int) []stri
func (m *Module) buildMemory() (mem *MemoryInstance) {
memSec := m.MemorySection
if memSec != nil {
min := MemoryPagesToBytesNum(memSec.Min)
capacity := MemoryPagesToBytesNum(memSec.Cap)
mem = &MemoryInstance{
Buffer: make([]byte, MemoryPagesToBytesNum(memSec.Min)),
Buffer: make([]byte, min, capacity),
Min: memSec.Min,
Cap: memSec.Cap,
Max: memSec.Max,
}
}
@@ -652,11 +655,38 @@ type limitsType struct {
// Memory describes the limits of pages (64KB) in a memory.
type Memory struct {
Min, Max uint32
// IsMaxEncoded true if the Max is encoded in the orignial source (binary or text).
Min, Cap, Max uint32
// IsMaxEncoded true if the Max is encoded in the original source (binary or text).
IsMaxEncoded bool
}
// ValidateMinMax ensures values assigned to Min and Max are within valid thresholds.
func (m *Memory) ValidateMinMax(memoryLimitPages uint32) error {
if !m.IsMaxEncoded {
m.Max = memoryLimitPages
}
min, max := m.Min, m.Max
if max > memoryLimitPages {
return fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)", max, PagesToUnitOfBytes(max), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
} else if min > memoryLimitPages {
return fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)", min, PagesToUnitOfBytes(min), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
} else if min > max {
return fmt.Errorf("min %d pages (%s) > max %d pages (%s)", min, PagesToUnitOfBytes(min), max, PagesToUnitOfBytes(max))
}
return nil
}
// ValidateCap ensures the value assigned to Cap is within valid thresholds.
func (m *Memory) ValidateCap(memoryLimitPages uint32) error {
capacity, min := m.Cap, m.Min
if capacity < min {
return fmt.Errorf("capacity %d pages (%s) less than minimum %d pages (%s)", capacity, PagesToUnitOfBytes(capacity), min, PagesToUnitOfBytes(min))
} else if capacity > memoryLimitPages {
return fmt.Errorf("capacity %d pages (%s) over limit of %d pages (%s)", capacity, PagesToUnitOfBytes(capacity), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
}
return nil
}
type GlobalType struct {
ValType ValueType
Mutable bool

View File

@@ -90,6 +90,83 @@ func TestExternTypeName(t *testing.T) {
}
}
func TestMemory_ValidateCap(t *testing.T) {
tests := []struct {
name string
mem *Memory
expectedErr string
}{
{
name: "ok",
mem: &Memory{Min: 2, Cap: 2},
},
{
name: "cap < min",
mem: &Memory{Min: 2, Cap: 1},
expectedErr: "capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
},
{
name: "cap > maxLimit",
mem: &Memory{Min: 2, Cap: 4},
expectedErr: "capacity 4 pages (256 Ki) over limit of 3 pages (192 Ki)",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := tc.mem.ValidateCap(3)
if tc.expectedErr == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestMemory_ValidateMinMax(t *testing.T) {
tests := []struct {
name string
mem *Memory
expectedErr string
}{
{
name: "ok",
mem: &Memory{Min: 2},
},
{
name: "max < min",
mem: &Memory{Min: 2, Max: 0, IsMaxEncoded: true},
expectedErr: "min 2 pages (128 Ki) > max 0 pages (0 Ki)",
},
{
name: "min > limit",
mem: &Memory{Min: math.MaxUint32},
expectedErr: "min 4294967295 pages (3 Ti) over limit of 3 pages (192 Ki)",
},
{
name: "max > limit",
mem: &Memory{Max: math.MaxUint32, IsMaxEncoded: true},
expectedErr: "max 4294967295 pages (3 Ti) over limit of 3 pages (192 Ki)",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := tc.mem.ValidateMinMax(3)
if tc.expectedErr == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
func TestModule_allDeclarations(t *testing.T) {
for i, tc := range []struct {
module *Module
@@ -730,7 +807,7 @@ func TestModule_buildMemoryInstance(t *testing.T) {
t.Run("non-nil", func(t *testing.T) {
min := uint32(1)
max := uint32(10)
m := Module{MemorySection: &Memory{Min: min, Max: max}}
m := Module{MemorySection: &Memory{Min: min, Cap: min, Max: max}}
mem := m.buildMemory()
require.Equal(t, min, mem.Min)
require.Equal(t, max, mem.Max)

View File

@@ -28,16 +28,16 @@ func TestModuleInstance_Memory(t *testing.T) {
},
{
name: "memory not exported",
input: &Module{MemorySection: &Memory{Min: 1}},
input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
},
{
name: "memory not exported, one page",
input: &Module{MemorySection: &Memory{Min: 1}},
input: &Module{MemorySection: &Memory{Min: 1, Cap: 1}},
},
{
name: "memory exported, different name",
input: &Module{
MemorySection: &Memory{Min: 1},
MemorySection: &Memory{Min: 1, Cap: 1},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "momory", Index: 0}},
},
},
@@ -52,7 +52,7 @@ func TestModuleInstance_Memory(t *testing.T) {
{
name: "memory exported, one page",
input: &Module{
MemorySection: &Memory{Min: 1},
MemorySection: &Memory{Min: 1, Cap: 1},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
},
expected: true,
@@ -61,7 +61,7 @@ func TestModuleInstance_Memory(t *testing.T) {
{
name: "memory exported, two pages",
input: &Module{
MemorySection: &Memory{Min: 2},
MemorySection: &Memory{Min: 2, Cap: 2},
ExportSection: []*Export{{Type: ExternTypeMemory, Name: "memory", Index: 0}},
},
expected: true,
@@ -156,7 +156,7 @@ func TestStore_CloseModule(t *testing.T) {
_, err := s.Instantiate(testCtx, &Module{
TypeSection: []*FunctionType{{}},
ImportSection: []*Import{{Type: ExternTypeFunc, Module: importedModuleName, Name: "fn", DescFunc: 0}},
MemorySection: &Memory{Min: 1},
MemorySection: &Memory{Min: 1, Cap: 1},
GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
TableSection: &Table{Min: 10},
}, importingModuleName, nil, nil)
@@ -205,7 +205,7 @@ func TestStore_hammer(t *testing.T) {
TypeSection: []*FunctionType{{}},
FunctionSection: []uint32{0},
CodeSection: []*Code{{Body: []byte{OpcodeEnd}}},
MemorySection: &Memory{Min: 1},
MemorySection: &Memory{Min: 1, Cap: 1},
GlobalSection: []*Global{{Type: &GlobalType{}, Init: &ConstantExpression{Opcode: OpcodeI32Const, Data: const1}}},
TableSection: &Table{Min: 10},
ImportSection: []*Import{
@@ -352,7 +352,7 @@ func TestCallContext_ExportedFunction(t *testing.T) {
importing, err := s.Instantiate(testCtx, &Module{
TypeSection: []*FunctionType{{}},
ImportSection: []*Import{{Type: ExternTypeFunc, Module: "host", Name: "host_fn", DescFunc: 0}},
MemorySection: &Memory{Min: 1},
MemorySection: &Memory{Min: 1, Cap: 1},
ExportSection: []*Export{{Type: ExternTypeFunc, Name: "host.fn", Index: 0}},
}, "test", nil, nil)
require.NoError(t, err)
@@ -636,10 +636,10 @@ func TestStore_resolveImports(t *testing.T) {
})
t.Run("minimum size mismatch", func(t *testing.T) {
s := newStore()
importMemoryType := &Memory{Min: 2}
importMemoryType := &Memory{Min: 2, Cap: 2}
s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
Type: ExternTypeMemory,
Memory: &MemoryInstance{Min: importMemoryType.Min - 1},
Memory: &MemoryInstance{Min: importMemoryType.Min - 1, Cap: 2},
}}, Name: moduleName}
_, _, _, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}})
require.EqualError(t, err, "import[0] memory[test.target]: minimum size mismatch: 2 > 1")
@@ -650,7 +650,7 @@ func TestStore_resolveImports(t *testing.T) {
importMemoryType := &Memory{Max: max}
s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: {
Type: ExternTypeMemory,
Memory: &MemoryInstance{Max: MemoryMaxPages},
Memory: &MemoryInstance{Max: MemoryLimitPages},
}}, Name: moduleName}
_, _, _, _, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: importMemoryType}}})
require.EqualError(t, err, "import[0] memory[test.target]: maximum size mismatch: 10 < 65536")

View File

@@ -104,7 +104,7 @@ type moduleParser struct {
// DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Text Format
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0
func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages uint32) (result *wasm.Module, err error) {
func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryLimitPages uint32) (result *wasm.Module, err error) {
// TODO: when globals are supported, err on global vars if disabled
// names are the wasm.Module NameSection
@@ -114,7 +114,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages u
// * LocalNames: nil when neither imported nor module-defined functions had named (param) fields.
names := &wasm.NameSection{}
module := &wasm.Module{NameSection: names}
p := newModuleParser(module, enabledFeatures, memoryMaxPages)
p := newModuleParser(module, enabledFeatures, memoryLimitPages)
p.source = source
// A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this
@@ -143,7 +143,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages u
return module, nil
}
func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryMaxPages uint32) *moduleParser {
func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryLimitPages uint32) *moduleParser {
p := moduleParser{module: module, enabledFeatures: enabledFeatures,
typeNamespace: newIndexNamespace(module.SectionElementCount),
funcNamespace: newIndexNamespace(module.SectionElementCount),
@@ -152,7 +152,7 @@ func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryM
p.typeParser = newTypeParser(enabledFeatures, p.typeNamespace, p.onTypeEnd)
p.typeUseParser = newTypeUseParser(enabledFeatures, module, p.typeNamespace)
p.funcParser = newFuncParser(enabledFeatures, p.typeUseParser, p.funcNamespace, p.endFunc)
p.memoryParser = newMemoryParser(memoryMaxPages, p.memoryNamespace, p.endMemory)
p.memoryParser = newMemoryParser(memoryLimitPages, p.memoryNamespace, p.endMemory)
return &p
}

View File

@@ -1226,14 +1226,14 @@ func TestDecodeModule(t *testing.T) {
name: "memory",
input: "(module (memory 1))",
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages},
},
},
{
name: "memory ID",
input: "(module (memory $mem 1))",
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages},
},
},
{
@@ -1420,7 +1420,7 @@ func TestDecodeModule(t *testing.T) {
(export "foo" (memory 0))
)`,
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages},
ExportSection: []*wasm.Export{
{Name: "foo", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -1433,7 +1433,7 @@ func TestDecodeModule(t *testing.T) {
(memory 0)
)`,
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryLimitPages},
ExportSection: []*wasm.Export{
{Name: "foo", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -1465,7 +1465,7 @@ func TestDecodeModule(t *testing.T) {
(export "memory" (memory $mem))
)`,
expected: &wasm.Module{
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages},
MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryLimitPages},
ExportSection: []*wasm.Export{
{Name: "memory", Type: wasm.ExternTypeMemory, Index: 0},
},
@@ -1564,7 +1564,7 @@ func TestDecodeModule(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
m, err := DecodeModule([]byte(tc.input), wasm.FeaturesFinished, wasm.MemoryMaxPages)
m, err := DecodeModule([]byte(tc.input), wasm.FeaturesFinished, wasm.MemoryLimitPages)
require.NoError(t, err)
require.Equal(t, tc.expected, m)
})
@@ -1573,9 +1573,9 @@ func TestDecodeModule(t *testing.T) {
func TestParseModule_Errors(t *testing.T) {
tests := []struct {
name, input string
memoryMaxPages uint32
expectedErr string
name, input string
memoryLimitPages uint32
expectedErr string
}{
{
name: "forgot parens",
@@ -1998,10 +1998,10 @@ func TestParseModule_Errors(t *testing.T) {
expectedErr: "2:47: i32.trunc_sat_f32_s invalid as feature \"nontrapping-float-to-int-conversion\" is disabled in module.func[0]",
},
{
name: "memory over max",
input: "(module (memory 1 4))",
memoryMaxPages: 3,
expectedErr: "1:19: max 4 pages (256 Ki) outside range of 3 pages (192 Ki) in module.memory[0]",
name: "memory over max",
input: "(module (memory 1 4))",
memoryLimitPages: 3,
expectedErr: "1:19: max 4 pages (256 Ki) over limit of 3 pages (192 Ki) in module.memory[0]",
},
{
name: "second memory",
@@ -2145,10 +2145,10 @@ func TestParseModule_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
if tc.memoryMaxPages == 0 {
tc.memoryMaxPages = wasm.MemoryMaxPages
if tc.memoryLimitPages == 0 {
tc.memoryLimitPages = wasm.MemoryLimitPages
}
_, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryMaxPages)
_, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryLimitPages)
require.EqualError(t, err, tc.expectedErr)
})
}

View File

@@ -7,13 +7,13 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
func newMemoryParser(memoryMaxPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser {
return &memoryParser{memoryMaxPages: memoryMaxPages, memoryNamespace: memoryNamespace, onMemory: onMemory}
func newMemoryParser(memoryLimitPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser {
return &memoryParser{memoryLimitPages: memoryLimitPages, memoryNamespace: memoryNamespace, onMemory: onMemory}
}
type onMemory func(min, max uint32, maxDecooded bool) tokenParser
type onMemory func(min, max uint32, maxDecoded bool) tokenParser
// memoryParser parses a api.Memory from and dispatches to onMemory.
// memoryParser parses an api.Memory from and dispatches to onMemory.
//
// Ex. `(module (memory 0 1024))`
// starts here --^ ^
@@ -22,9 +22,9 @@ type onMemory func(min, max uint32, maxDecooded bool) tokenParser
// Note: memoryParser is reusable. The caller resets via begin.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A7
type memoryParser struct {
// memoryMaxPages is the limit of pages (not bytes) for each wasm.Memory.
memoryMaxPages uint32
maxDecoded bool
// memoryLimitPages is the limit of pages (not bytes) for each wasm.Memory.
memoryLimitPages uint32
maxDecoded bool
memoryNamespace *indexNamespace
@@ -50,7 +50,7 @@ type memoryParser struct {
// calls beginMin --^
func (p *memoryParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
p.currentMin = 0
p.currentMax = p.memoryMaxPages
p.currentMax = p.memoryLimitPages
if tok == tokenID { // Ex. $mem
if _, err := p.memoryNamespace.setID(tokenBytes); err != nil {
return nil, err
@@ -66,8 +66,8 @@ func (p *memoryParser) beginMin(tok tokenType, tokenBytes []byte, _, _ uint32) (
case tokenID: // Ex.(memory $rf32 $rf32
return nil, fmt.Errorf("redundant ID %s", tokenBytes)
case tokenUN:
if i, overflow := decodeUint32(tokenBytes); overflow || i > p.memoryMaxPages {
return nil, fmt.Errorf("min %d pages (%s) outside range of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryMaxPages, wasm.PagesToUnitOfBytes(p.memoryMaxPages))
if i, overflow := decodeUint32(tokenBytes); overflow || i > p.memoryLimitPages {
return nil, fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryLimitPages, wasm.PagesToUnitOfBytes(p.memoryLimitPages))
} else {
p.currentMin = i
}
@@ -85,8 +85,8 @@ func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint
switch tok {
case tokenUN:
i, overflow := decodeUint32(tokenBytes)
if overflow || i > p.memoryMaxPages {
return nil, fmt.Errorf("max %d pages (%s) outside range of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryMaxPages, wasm.PagesToUnitOfBytes(p.memoryMaxPages))
if overflow || i > p.memoryLimitPages {
return nil, fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)", i, wasm.PagesToUnitOfBytes(i), p.memoryLimitPages, wasm.PagesToUnitOfBytes(p.memoryLimitPages))
} else if i < p.currentMin {
return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", p.currentMin, wasm.PagesToUnitOfBytes(p.currentMin), i, wasm.PagesToUnitOfBytes(i))
}

View File

@@ -9,7 +9,7 @@ import (
func TestMemoryParser(t *testing.T) {
zero := uint32(0)
max := wasm.MemoryMaxPages
max := wasm.MemoryLimitPages
tests := []struct {
name string
input string
@@ -122,12 +122,12 @@ func TestMemoryParser_Errors(t *testing.T) {
{
name: "min > limit",
input: "(memory 4294967295)",
expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
expectedErr: "min 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
},
{
name: "max > limit",
input: "(memory 0 4294967295)",
expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)",
expectedErr: "max 4294967295 pages (3 Ti) over limit of 65536 pages (4 Gi)",
},
}
@@ -166,7 +166,7 @@ func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.Memor
parsed = &wasm.Memory{Min: min, Max: max, IsMaxEncoded: maxDecoded}
return parseErr
}
tp := newMemoryParser(wasm.MemoryMaxPages, memoryNamespace, setFunc)
tp := newMemoryParser(wasm.MemoryLimitPages, memoryNamespace, setFunc)
// memoryParser starts after the '(memory', so we need to eat it first!
_, _, err := lex(skipTokens(2, tp.begin), []byte(input))
return parsed, tp, err

View File

@@ -558,7 +558,7 @@ func requireCompilationResult(t *testing.T, enabledFeatures wasm.Features, expec
}
func requireModuleText(t *testing.T, source string) *wasm.Module {
m, err := text.DecodeModule([]byte(source), wasm.FeaturesFinished, wasm.MemoryMaxPages)
m, err := text.DecodeModule([]byte(source), wasm.FeaturesFinished, wasm.MemoryLimitPages)
require.NoError(t, err)
return m
}

52
wasm.go
View File

@@ -124,17 +124,19 @@ func NewRuntime() Runtime {
// NewRuntimeWithConfig returns a runtime with the given configuration.
func NewRuntimeWithConfig(config *RuntimeConfig) Runtime {
return &runtime{
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
enabledFeatures: config.enabledFeatures,
memoryMaxPages: config.memoryMaxPages,
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
enabledFeatures: config.enabledFeatures,
memoryLimitPages: config.memoryLimitPages,
memoryCapacityPages: config.memoryCapacityPages,
}
}
// runtime allows decoupling of public interfaces from internal representation.
type runtime struct {
enabledFeatures wasm.Features
store *wasm.Store
memoryMaxPages uint32
enabledFeatures wasm.Features
store *wasm.Store
memoryLimitPages uint32
memoryCapacityPages func(minPages uint32, maxPages *uint32) uint32
}
// Module implements Runtime.Module
@@ -160,13 +162,14 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCo
decoder = text.DecodeModule
}
if r.memoryMaxPages > wasm.MemoryMaxPages {
return nil, fmt.Errorf("memoryMaxPages %d (%s) > specification max %d (%s)",
r.memoryMaxPages, wasm.PagesToUnitOfBytes(r.memoryMaxPages),
wasm.MemoryMaxPages, wasm.PagesToUnitOfBytes(wasm.MemoryMaxPages))
if r.memoryLimitPages > wasm.MemoryLimitPages {
return nil, fmt.Errorf("memoryLimitPages %d (%s) > specification max %d (%s)",
r.memoryLimitPages, wasm.PagesToUnitOfBytes(r.memoryLimitPages),
wasm.MemoryLimitPages, wasm.PagesToUnitOfBytes(wasm.MemoryLimitPages))
}
internal, err := decoder(source, r.enabledFeatures, r.memoryMaxPages)
internal, err := decoder(source, r.enabledFeatures, r.memoryLimitPages)
if err != nil {
return nil, err
} else if err = internal.Validate(r.enabledFeatures); err != nil {
@@ -175,6 +178,20 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCo
return nil, err
}
// Determine the correct memory capacity, if a memory was defined.
if mem := internal.MemorySection; mem != nil {
memoryName := "0"
for _, e := range internal.ExportSection {
if e.Type == wasm.ExternTypeMemory {
memoryName = e.Name
break
}
}
if err = r.setMemoryCapacity(memoryName, mem); err != nil {
return nil, err
}
}
internal.AssignModuleID(source)
if err = r.store.Engine.CompileModule(ctx, internal); err != nil {
@@ -252,3 +269,16 @@ func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled *Com
}
return
}
// setMemoryCapacity sets wasm.Memory cap using the function supplied by RuntimeConfig.WithMemoryCapacityPages.
func (r *runtime) setMemoryCapacity(name string, mem *wasm.Memory) error {
var max *uint32
if mem.IsMaxEncoded {
max = &mem.Max
}
mem.Cap = r.memoryCapacityPages(mem.Min, max)
if err := mem.ValidateCap(r.memoryLimitPages); err != nil {
return fmt.Errorf("memory[%s] %v", name, err)
}
return nil
}

View File

@@ -18,9 +18,10 @@ import (
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
func TestRuntime_DecodeModule(t *testing.T) {
func TestRuntime_CompileModule(t *testing.T) {
tests := []struct {
name string
runtime Runtime
source []byte
expectedName string
}{
@@ -66,9 +67,27 @@ func TestRuntime_DecodeModule(t *testing.T) {
require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine)
})
}
t.Run("text - memory", func(t *testing.T) {
r := NewRuntimeWithConfig(NewRuntimeConfig().
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 2 }))
source := []byte(`(module (memory 1 3))`)
code, err := r.CompileModule(testCtx, source)
require.NoError(t, err)
defer code.Close(testCtx)
require.Equal(t, &wasm.Memory{
Min: 1,
Cap: 2, // Uses capacity function
Max: 3,
IsMaxEncoded: true,
}, code.module.MemorySection)
})
}
func TestRuntime_DecodeModule_Errors(t *testing.T) {
func TestRuntime_CompileModule_Errors(t *testing.T) {
tests := []struct {
name string
runtime Runtime
@@ -90,22 +109,36 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) {
expectedErr: "1:2: unexpected field: modular",
},
{
name: "RuntimeConfig.memoryMaxPage too large",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(math.MaxUint32)),
name: "RuntimeConfig.memoryLimitPages too large",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(math.MaxUint32)),
source: []byte(`(module)`),
expectedErr: "memoryMaxPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
expectedErr: "memoryLimitPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)",
},
{
name: "memory has too many pages - text",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)),
source: []byte(`(module (memory 3))`),
expectedErr: "1:17: min 3 pages (192 Ki) outside range of 2 pages (128 Ki) in module.memory[0]",
expectedErr: "1:17: min 3 pages (192 Ki) over limit of 2 pages (128 Ki) in module.memory[0]",
},
{
name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go
runtime: NewRuntimeWithConfig(NewRuntimeConfig().
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })),
source: []byte(`(module (memory 3))`),
expectedErr: "memory[0] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
},
{
name: "memory cap < min - exported", // only one test to avoid duplicating tests in module_test.go
runtime: NewRuntimeWithConfig(NewRuntimeConfig().
WithMemoryCapacityPages(func(minPages uint32, maxPages *uint32) uint32 { return 1 })),
source: []byte(`(module (memory 3) (export "memory" (memory 0)))`),
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
},
{
name: "memory has too many pages - binary",
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)),
runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryLimitPages(2)),
source: binary.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Max: 3, IsMaxEncoded: true}}),
expectedErr: "section memory: max 3 pages (192 Ki) outside range of 2 pages (128 Ki)",
expectedErr: "section memory: max 3 pages (192 Ki) over limit of 2 pages (128 Ki)",
},
}
@@ -124,6 +157,52 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) {
}
}
func TestRuntime_setMemoryCapacity(t *testing.T) {
tests := []struct {
name string
runtime *runtime
mem *wasm.Memory
expectedErr string
}{
{
name: "cap ok",
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
return 3
}, memoryLimitPages: 3},
mem: &wasm.Memory{Min: 2},
},
{
name: "cap < min",
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
return 1
}, memoryLimitPages: 3},
mem: &wasm.Memory{Min: 2},
expectedErr: "memory[memory] capacity 1 pages (64 Ki) less than minimum 2 pages (128 Ki)",
},
{
name: "cap > maxLimit",
runtime: &runtime{memoryCapacityPages: func(minPages uint32, maxPages *uint32) uint32 {
return 4
}, memoryLimitPages: 3},
mem: &wasm.Memory{Min: 2},
expectedErr: "memory[memory] capacity 4 pages (256 Ki) over limit of 3 pages (192 Ki)",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := tc.runtime.setMemoryCapacity("memory", tc.mem)
if tc.expectedErr == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.expectedErr)
}
})
}
}
// TestModule_Memory only covers a couple cases to avoid duplication of internal/wasm/runtime_test.go
func TestModule_Memory(t *testing.T) {
tests := []struct {