From 2c03098dbaa8ec5ab0cf5b1344f0f0ef76933d31 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Fri, 29 Apr 2022 17:54:48 +0800 Subject: [PATCH] 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 --- builder.go | 28 ++++-- builder_test.go | 32 +++--- config.go | 64 ++++++++---- config_test.go | 24 ++++- .../integration_test/spectest/encoder_test.go | 2 +- .../integration_test/spectest/spectest.go | 18 +++- internal/integration_test/vs/codec.go | 2 +- internal/integration_test/vs/codec_test.go | 8 +- internal/modgen/modgen.go | 10 +- internal/modgen/modgen_test.go | 6 +- internal/wasm/binary/decoder.go | 6 +- internal/wasm/binary/decoder_test.go | 22 ++--- internal/wasm/binary/import.go | 4 +- internal/wasm/binary/import_test.go | 2 +- internal/wasm/binary/memory.go | 15 +-- internal/wasm/binary/memory_test.go | 24 ++--- internal/wasm/binary/section.go | 8 +- internal/wasm/binary/section_test.go | 16 +-- internal/wasm/memory.go | 28 ++++-- internal/wasm/memory_test.go | 68 +++++++++---- internal/wasm/module.go | 38 +++++++- internal/wasm/module_test.go | 79 ++++++++++++++- internal/wasm/store_test.go | 22 ++--- internal/wasm/text/decoder.go | 8 +- internal/wasm/text/decoder_test.go | 32 +++--- internal/wasm/text/memory_parser.go | 24 ++--- internal/wasm/text/memory_parser_test.go | 8 +- internal/wazeroir/compiler_test.go | 2 +- wasm.go | 52 +++++++--- wasm_test.go | 97 +++++++++++++++++-- 30 files changed, 532 insertions(+), 217 deletions(-) diff --git a/builder.go b/builder.go index cb44181f..7432df20 100644 --- a/builder.go +++ b/builder.go @@ -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 } } diff --git a/builder_test.go b/builder_test.go index 877883f5..f7a7dfd6 100644 --- a/builder_test.go +++ b/builder_test.go @@ -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) }) } diff --git a/config.go b/config.go index 3a3942f2..2a75add9 100644 --- a/config.go +++ b/config.go @@ -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 diff --git a/config_test.go b/config_test.go index 66d6a86d..0c871a13 100644 --- a/config_test.go +++ b/config_test.go @@ -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) { diff --git a/internal/integration_test/spectest/encoder_test.go b/internal/integration_test/spectest/encoder_test.go index d11dd855..8f602243 100644 --- a/internal/integration_test/spectest/encoder_test.go +++ b/internal/integration_test/spectest/encoder_test.go @@ -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) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index a5605e24..031dc7f8 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -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 diff --git a/internal/integration_test/vs/codec.go b/internal/integration_test/vs/codec.go index 3f07825e..12e79140 100644 --- a/internal/integration_test/vs/codec.go +++ b/internal/integration_test/vs/codec.go @@ -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) diff --git a/internal/integration_test/vs/codec_test.go b/internal/integration_test/vs/codec_test.go index 6fc18825..585edabb 100644 --- a/internal/integration_test/vs/codec_test.go +++ b/internal/integration_test/vs/codec_test.go @@ -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) } } diff --git a/internal/modgen/modgen.go b/internal/modgen/modgen.go index e65ae02c..f23ccbdd 100644 --- a/internal/modgen/modgen.go +++ b/internal/modgen/modgen.go @@ -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 { diff --git a/internal/modgen/modgen_test.go b/internal/modgen/modgen_test.go index 7950b4b5..e5801066 100644 --- a/internal/modgen/modgen_test.go +++ b/internal/modgen/modgen_test.go @@ -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}, }, diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 0f6a3fac..aa0e37a9 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -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. diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go index 97e9246b..b5363709 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -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) }) } diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index 174cf9fe..c9d67408 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -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: diff --git a/internal/wasm/binary/import_test.go b/internal/wasm/binary/import_test.go index 81923ca9..ef562960 100644 --- a/internal/wasm/binary/import_test.go +++ b/internal/wasm/binary/import_test.go @@ -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', diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go index bbed69ef..f26dfe7d 100644 --- a/internal/wasm/binary/memory.go +++ b/internal/wasm/binary/memory.go @@ -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. diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go index 341a4240..27f22827 100644 --- a/internal/wasm/binary/memory_test.go +++ b/internal/wasm/binary/memory_test.go @@ -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) }) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 4da0a8a7..271c3276 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -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) { diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index d5c1a306..d4db63b1 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -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) }) } diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go index 95560be5..2f5c4e82 100644 --- a/internal/wasm/memory.go +++ b/internal/wasm/memory.go @@ -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 } } diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go index ee44d5a4..ee19e628 100644 --- a/internal/wasm/memory_test.go +++ b/internal/wasm/memory_test.go @@ -11,7 +11,7 @@ import ( func TestMemoryPageConsts(t *testing.T) { require.Equal(t, MemoryPageSize, uint32(1)< 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 diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 45628fce..5a79076f 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -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) diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 5264542d..0ed0309b 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -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") diff --git a/internal/wasm/text/decoder.go b/internal/wasm/text/decoder.go index 8be7db03..2fb11e98 100644 --- a/internal/wasm/text/decoder.go +++ b/internal/wasm/text/decoder.go @@ -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 } diff --git a/internal/wasm/text/decoder_test.go b/internal/wasm/text/decoder_test.go index 149a01d9..969ffb31 100644 --- a/internal/wasm/text/decoder_test.go +++ b/internal/wasm/text/decoder_test.go @@ -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) }) } diff --git a/internal/wasm/text/memory_parser.go b/internal/wasm/text/memory_parser.go index 28884472..e799b4a6 100644 --- a/internal/wasm/text/memory_parser.go +++ b/internal/wasm/text/memory_parser.go @@ -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)) } diff --git a/internal/wasm/text/memory_parser_test.go b/internal/wasm/text/memory_parser_test.go index 1c9921d3..2f8b18c5 100644 --- a/internal/wasm/text/memory_parser_test.go +++ b/internal/wasm/text/memory_parser_test.go @@ -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 diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go index d2cf8ead..61fc243e 100644 --- a/internal/wazeroir/compiler_test.go +++ b/internal/wazeroir/compiler_test.go @@ -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 } diff --git a/wasm.go b/wasm.go index e0ca7c84..96d0e644 100644 --- a/wasm.go +++ b/wasm.go @@ -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 +} diff --git a/wasm_test.go b/wasm_test.go index 295fe8e2..9ae7953a 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -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 {