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 {