From 8f461f6f12dcf2194cb1a0f98f89b81a8f64e1e1 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Thu, 31 Mar 2022 08:57:28 +0800 Subject: [PATCH] Makes memory limit configurable and a compile error (#419) This allows users to reduce the memory limit per module below 4 Gi. This is often needed because Wasm routinely leaves off the max, which implies spec max (4 Gi). This uses Ki Gi etc in error messages because the spec chooses to, though we can change to make it less awkward. This also fixes an issue where we instantiated an engine inside config. Signed-off-by: Adrian Cole --- config.go | 85 ++++++++++++++++------- config_test.go | 68 +++++++++++++++++- internal/wasi/wasi_test.go | 2 +- internal/wasm/binary/decoder.go | 6 +- internal/wasm/binary/decoder_test.go | 21 +++--- internal/wasm/binary/encoder_test.go | 8 +-- internal/wasm/binary/import.go | 4 +- internal/wasm/binary/memory.go | 25 ++++--- internal/wasm/binary/memory_test.go | 38 +++++----- internal/wasm/binary/section.go | 8 +-- internal/wasm/binary/section_test.go | 17 +++-- internal/wasm/memory.go | 36 +++++++--- internal/wasm/memory_test.go | 88 ++++++++++++++++-------- internal/wasm/module.go | 6 +- internal/wasm/module_test.go | 10 +-- internal/wasm/store.go | 32 ++++----- internal/wasm/store_test.go | 12 ++-- internal/wasm/text/decoder.go | 14 ++-- internal/wasm/text/decoder_test.go | 26 ++++--- internal/wasm/text/memory_parser.go | 28 ++++---- internal/wasm/text/memory_parser_test.go | 24 +++---- tests/bench/memory_bench_test.go | 3 +- tests/spectest/spec_test.go | 7 +- vs/codec_test.go | 12 ++-- wasm.go | 13 +++- wasm_test.go | 25 ++++++- 26 files changed, 410 insertions(+), 208 deletions(-) diff --git a/config.go b/config.go index 3cb114a0..d1469774 100644 --- a/config.go +++ b/config.go @@ -13,32 +13,46 @@ import ( "github.com/tetratelabs/wazero/internal/wasm/jit" ) +// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig +type RuntimeConfig struct { + newEngine func() internalwasm.Engine + ctx context.Context + enabledFeatures internalwasm.Features + memoryMaxPages uint32 +} + +// engineLessConfig helps avoid copy/pasting the wrong defaults. +var engineLessConfig = &RuntimeConfig{ + ctx: context.Background(), + enabledFeatures: internalwasm.Features20191205, + memoryMaxPages: internalwasm.MemoryMaxPages, +} + +// clone ensures all fields are coped even if nil. +func (c *RuntimeConfig) clone() *RuntimeConfig { + return &RuntimeConfig{ + newEngine: c.newEngine, + ctx: c.ctx, + enabledFeatures: c.enabledFeatures, + memoryMaxPages: c.memoryMaxPages, + } +} + // NewRuntimeConfigJIT compiles WebAssembly modules into runtime.GOARCH-specific assembly for optimal performance. // // Note: This panics at runtime the runtime.GOOS or runtime.GOARCH does not support JIT. Use NewRuntimeConfig to safely // detect and fallback to NewRuntimeConfigInterpreter if needed. func NewRuntimeConfigJIT() *RuntimeConfig { - return &RuntimeConfig{ - engine: jit.NewEngine(), - ctx: context.Background(), - enabledFeatures: internalwasm.Features20191205, - } + ret := engineLessConfig.clone() + ret.newEngine = jit.NewEngine + return ret } // NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly. func NewRuntimeConfigInterpreter() *RuntimeConfig { - return &RuntimeConfig{ - engine: interpreter.NewEngine(), - ctx: context.Background(), - enabledFeatures: internalwasm.Features20191205, - } -} - -// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig -type RuntimeConfig struct { - engine internalwasm.Engine - ctx context.Context - enabledFeatures internalwasm.Features + ret := engineLessConfig.clone() + ret.newEngine = interpreter.NewEngine + return ret } // WithContext sets the default context used to initialize the module. Defaults to context.Background if nil. @@ -49,11 +63,29 @@ type RuntimeConfig struct { // * This is the default context of wasm.Function when callers pass nil. // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-function%E2%91%A0 -func (r *RuntimeConfig) WithContext(ctx context.Context) *RuntimeConfig { +func (c *RuntimeConfig) WithContext(ctx context.Context) *RuntimeConfig { if ctx == nil { ctx = context.Background() } - return &RuntimeConfig{engine: r.engine, ctx: ctx, enabledFeatures: r.enabledFeatures} + ret := c.clone() + ret.ctx = ctx + return ret +} + +// WithMemoryMaxPages reduces 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). +// * 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/wasm-core-1/#memory-types%E2%91%A0 +func (c *RuntimeConfig) WithMemoryMaxPages(memoryMaxPages uint32) *RuntimeConfig { + ret := c.clone() + ret.memoryMaxPages = memoryMaxPages + return ret } // WithFeatureMutableGlobal allows globals to be mutable. This defaults to true as the feature was finished in @@ -61,19 +93,20 @@ func (r *RuntimeConfig) WithContext(ctx context.Context) *RuntimeConfig { // // When false, a wasm.Global can never be cast to a wasm.MutableGlobal, and any source that includes global vars // will fail to parse. -// -func (r *RuntimeConfig) WithFeatureMutableGlobal(enabled bool) *RuntimeConfig { - enabledFeatures := r.enabledFeatures.Set(internalwasm.FeatureMutableGlobal, enabled) - return &RuntimeConfig{engine: r.engine, ctx: r.ctx, enabledFeatures: enabledFeatures} +func (c *RuntimeConfig) WithFeatureMutableGlobal(enabled bool) *RuntimeConfig { + ret := c.clone() + ret.enabledFeatures = ret.enabledFeatures.Set(internalwasm.FeatureMutableGlobal, enabled) + return ret } // WithFeatureSignExtensionOps enables sign-extend operations. This defaults to false as the feature was not finished in // WebAssembly 1.0 (20191205). // // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md -func (r *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig { - enabledFeatures := r.enabledFeatures.Set(internalwasm.FeatureSignExtensionOps, enabled) - return &RuntimeConfig{engine: r.engine, ctx: r.ctx, enabledFeatures: enabledFeatures} +func (c *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig { + ret := c.clone() + ret.enabledFeatures = ret.enabledFeatures.Set(internalwasm.FeatureSignExtensionOps, enabled) + return ret } // Module is a WebAssembly 1.0 (20191205) module to instantiate. diff --git a/config_test.go b/config_test.go index 7bd7d628..6a81259e 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,7 @@ package wazero import ( + "context" "io" "math" "testing" @@ -11,7 +12,72 @@ import ( internalwasm "github.com/tetratelabs/wazero/internal/wasm" ) -func TestRuntimeConfig_Features(t *testing.T) { +func TestRuntimeConfig(t *testing.T) { + tests := []struct { + name string + with func(*RuntimeConfig) *RuntimeConfig + expected *RuntimeConfig + }{ + { + name: "WithContext", + with: func(c *RuntimeConfig) *RuntimeConfig { + return c.WithContext(context.TODO()) + }, + expected: &RuntimeConfig{ + ctx: context.TODO(), + }, + }, + { + name: "WithContext - nil", + with: func(c *RuntimeConfig) *RuntimeConfig { + return c.WithContext(nil) //nolint + }, + expected: &RuntimeConfig{ + ctx: context.Background(), + }, + }, + { + name: "WithMemoryMaxPages", + with: func(c *RuntimeConfig) *RuntimeConfig { + return c.WithMemoryMaxPages(1) + }, + expected: &RuntimeConfig{ + memoryMaxPages: 1, + }, + }, + { + name: "mutable-global", + with: func(c *RuntimeConfig) *RuntimeConfig { + return c.WithFeatureMutableGlobal(true) + }, + expected: &RuntimeConfig{ + enabledFeatures: internalwasm.FeatureMutableGlobal, + }, + }, + { + name: "sign-extension-ops", + with: func(c *RuntimeConfig) *RuntimeConfig { + return c.WithFeatureSignExtensionOps(true) + }, + expected: &RuntimeConfig{ + enabledFeatures: internalwasm.FeatureSignExtensionOps, + }, + }, + } + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + input := &RuntimeConfig{} + rc := tc.with(input) + require.Equal(t, tc.expected, rc) + // The source wasn't modified + require.Equal(t, &RuntimeConfig{}, input) + }) + } +} + +func TestRuntimeConfig_FeatureToggle(t *testing.T) { tests := []struct { name string feature internalwasm.Features diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go index f147cee3..75b5fd14 100644 --- a/internal/wasi/wasi_test.go +++ b/internal/wasi/wasi_test.go @@ -2198,7 +2198,7 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo (memory 1) ;; just an arbitrary size big enough for tests (export "memory" (memory 0)) (export "%[1]s" (func $wasi.%[1]s)) -)`, wasiFunction, wasiImport)), enabledFeatures) +)`, wasiFunction, wasiImport)), enabledFeatures, wasm.MemoryMaxPages) require.NoError(t, err) mod, err := store.Instantiate(ctx, m, moduleName, sysCtx) diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 301fdf26..2f02c566 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -11,7 +11,7 @@ import ( // DecodeModule implements internalwasm.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, features wasm.Features) (*wasm.Module, error) { +func DecodeModule(binary []byte, _ wasm.Features, memoryMaxPages uint32) (*wasm.Module, error) { r := bytes.NewReader(binary) // Magic number. @@ -71,7 +71,7 @@ func DecodeModule(binary []byte, features wasm.Features) (*wasm.Module, error) { case wasm.SectionIDType: m.TypeSection, err = decodeTypeSection(r) case wasm.SectionIDImport: - if m.ImportSection, err = decodeImportSection(r, features); err != nil { + if m.ImportSection, err = decodeImportSection(r, memoryMaxPages); err != nil { return nil, err // avoid re-wrapping the error. } case wasm.SectionIDFunction: @@ -79,7 +79,7 @@ func DecodeModule(binary []byte, features wasm.Features) (*wasm.Module, error) { case wasm.SectionIDTable: m.TableSection, err = decodeTableSection(r) case wasm.SectionIDMemory: - m.MemorySection, err = decodeMemorySection(r) + m.MemorySection, err = decodeMemorySection(r, memoryMaxPages) case wasm.SectionIDGlobal: if m.GlobalSection, err = decodeGlobalSection(r); 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 cf8d3850..8f3d4c1e 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -60,7 +60,7 @@ func TestDecodeModule(t *testing.T) { name: "table and memory section", input: &wasm.Module{ TableSection: &wasm.Table{Min: 3}, - MemorySection: &wasm.Memory{Min: 1}, + MemorySection: &wasm.Memory{Min: 1, Max: 1}, }, }, { @@ -81,7 +81,7 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205) + m, e := DecodeModule(EncodeModule(tc.input), wasm.Features20191205, wasm.MemoryMaxPages) require.NoError(t, e) require.Equal(t, tc.input, m) }) @@ -92,7 +92,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) + m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages) require.NoError(t, e) require.Equal(t, &wasm.Module{}, m) }) @@ -107,7 +107,7 @@ 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) + m, e := DecodeModule(input, wasm.Features20191205, wasm.MemoryMaxPages) require.NoError(t, e) require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m) }) @@ -115,10 +115,10 @@ func TestDecodeModule(t *testing.T) { func TestDecodeModule_Errors(t *testing.T) { tests := []struct { - name string - input []byte - features wasm.Features - expectedErr string + name string + input []byte + memoryMaxPages uint32 + expectedErr string }{ { name: "wrong magic", @@ -145,9 +145,12 @@ func TestDecodeModule_Errors(t *testing.T) { for _, tt := range tests { tc := tt + if tc.memoryMaxPages == 0 { + tc.memoryMaxPages = wasm.MemoryMaxPages + } t.Run(tc.name, func(t *testing.T) { - _, e := DecodeModule(tc.input, tc.features) + _, e := DecodeModule(tc.input, wasm.Features20191205, tc.memoryMaxPages) require.EqualError(t, e, tc.expectedErr) }) } diff --git a/internal/wasm/binary/encoder_test.go b/internal/wasm/binary/encoder_test.go index 97b54836..d3319f1a 100644 --- a/internal/wasm/binary/encoder_test.go +++ b/internal/wasm/binary/encoder_test.go @@ -109,15 +109,15 @@ func TestModule_Encode(t *testing.T) { name: "table and memory section", input: &wasm.Module{ TableSection: &wasm.Table{Min: 3}, - MemorySection: &wasm.Memory{Min: 1}, + MemorySection: &wasm.Memory{Min: 1, Max: 1}, }, expected: append(append(Magic, version...), wasm.SectionIDTable, 0x04, // 4 bytes in this section 0x01, // 1 table wasm.ElemTypeFuncref, 0x0, 0x03, // func, only min: 3 - wasm.SectionIDMemory, 0x03, // 3 bytes in this section - 0x01, // 1 memory - 0x0, 0x01, // only min: 01 + wasm.SectionIDMemory, 0x04, // 4 bytes in this section + 0x01, // 1 memory + 0x01, 0x01, 0x01, // min and max = 1 ), }, { diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index 0479da10..b4cd6bd6 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -8,7 +8,7 @@ import ( wasm "github.com/tetratelabs/wazero/internal/wasm" ) -func decodeImport(r *bytes.Reader, idx uint32, features wasm.Features) (i *wasm.Import, err error) { +func decodeImport(r *bytes.Reader, idx uint32, memoryMaxPages 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, features wasm.Features) (i *wasm. case wasm.ExternTypeTable: i.DescTable, err = decodeTable(r) case wasm.ExternTypeMemory: - i.DescMem, err = decodeMemory(r) + i.DescMem, err = decodeMemory(r, memoryMaxPages) case wasm.ExternTypeGlobal: i.DescGlobal, err = decodeGlobalType(r) default: diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go index 2d138902..d384582e 100644 --- a/internal/wasm/binary/memory.go +++ b/internal/wasm/binary/memory.go @@ -10,20 +10,23 @@ import ( // decodeMemory returns the wasm.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) (*wasm.Memory, error) { - min, max, err := decodeLimitsType(r) +func decodeMemory(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error) { + min, maxP, err := decodeLimitsType(r) if err != nil { return nil, err } - if min > wasm.MemoryMaxPages { - return nil, fmt.Errorf("memory min must be at most 65536 pages (4GiB)") + var max uint32 + if maxP != nil { + max = *maxP + } else { + max = memoryMaxPages } - if max != nil { - if *max < min { - return nil, fmt.Errorf("memory size minimum must not be greater than maximum") - } else if *max > wasm.MemoryMaxPages { - return nil, fmt.Errorf("memory max must be at most 65536 pages (4GiB)") - } + 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}, nil } @@ -32,5 +35,5 @@ func decodeMemory(r *bytes.Reader) (*wasm.Memory, error) { // // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory func encodeMemory(i *wasm.Memory) []byte { - return encodeLimitsType(i.Min, i.Max) + return encodeLimitsType(i.Min, &i.Max) } diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go index ab63f673..b3925afb 100644 --- a/internal/wasm/binary/memory_test.go +++ b/internal/wasm/binary/memory_test.go @@ -21,27 +21,27 @@ func TestMemoryType(t *testing.T) { }{ { name: "min 0", - input: &wasm.Memory{}, - expected: []byte{0x0, 0}, + input: &wasm.Memory{Max: wasm.MemoryMaxPages}, + expected: []byte{0x1, 0, 0x80, 0x80, 0x4}, }, { name: "min 0, max 0", - input: &wasm.Memory{Max: &zero}, + input: &wasm.Memory{Max: zero}, expected: []byte{0x1, 0, 0}, }, { - name: "min largest", - input: &wasm.Memory{Min: max}, - expected: []byte{0x0, 0x80, 0x80, 0x4}, + name: "min=max", + input: &wasm.Memory{Min: 1, Max: 1}, + expected: []byte{0x1, 1, 1}, }, { name: "min 0, max largest", - input: &wasm.Memory{Max: &max}, + input: &wasm.Memory{Max: max}, expected: []byte{0x1, 0, 0x80, 0x80, 0x4}, }, { name: "min largest max largest", - input: &wasm.Memory{Min: max, Max: &max}, + input: &wasm.Memory{Min: max, Max: max}, expected: []byte{0x1, 0x80, 0x80, 0x4, 0x80, 0x80, 0x4}, }, } @@ -55,7 +55,7 @@ func TestMemoryType(t *testing.T) { }) t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { - decoded, err := decodeMemory(bytes.NewReader(b)) + decoded, err := decodeMemory(bytes.NewReader(b), max) require.NoError(t, err) require.Equal(t, decoded, tc.input) }) @@ -64,31 +64,37 @@ func TestMemoryType(t *testing.T) { func TestDecodeMemoryType_Errors(t *testing.T) { tests := []struct { - name string - input []byte - expectedErr string + name string + input []byte + memoryMaxPages uint32 + expectedErr string }{ { name: "max < min", input: []byte{0x1, 0x80, 0x80, 0x4, 0}, - expectedErr: "memory size minimum must not be greater than maximum", + expectedErr: "min 65536 pages (4 Gi) > max 0 pages (0 Ki)", }, { name: "min > limit", input: []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xf}, - expectedErr: "memory min must be at most 65536 pages (4GiB)", + expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)", }, { name: "max > limit", input: []byte{0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf}, - expectedErr: "memory max must be at most 65536 pages (4GiB)", + expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)", }, } for _, tt := range tests { tc := tt + + if tc.memoryMaxPages == 0 { + tc.memoryMaxPages = wasm.MemoryMaxPages + } + t.Run(tc.name, func(t *testing.T) { - _, err := decodeMemory(bytes.NewReader(tc.input)) + _, err := decodeMemory(bytes.NewReader(tc.input), tc.memoryMaxPages) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index fafac522..23aa903f 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -61,7 +61,7 @@ func decodeFunctionType(r *bytes.Reader) (*wasm.FunctionType, error) { }, nil } -func decodeImportSection(r *bytes.Reader, features wasm.Features) ([]*wasm.Import, error) { +func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import, error) { vs, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("get size of vector: %w", err) @@ -69,7 +69,7 @@ func decodeImportSection(r *bytes.Reader, features wasm.Features) ([]*wasm.Impor result := make([]*wasm.Import, vs) for i := uint32(0); i < vs; i++ { - if result[i], err = decodeImport(r, i, features); err != nil { + if result[i], err = decodeImport(r, i, memoryMaxPages); err != nil { return nil, err } } @@ -103,7 +103,7 @@ func decodeTableSection(r *bytes.Reader) (*wasm.Table, error) { return decodeTable(r) } -func decodeMemorySection(r *bytes.Reader) (*wasm.Memory, error) { +func decodeMemorySection(r *bytes.Reader, memoryMaxPages uint32) (*wasm.Memory, error) { vs, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("error reading size") @@ -112,7 +112,7 @@ func decodeMemorySection(r *bytes.Reader) (*wasm.Memory, error) { return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs) } - return decodeMemory(r) + return decodeMemory(r, memoryMaxPages) } func decodeGlobalSection(r *bytes.Reader) ([]*wasm.Global, error) { diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go index 2de8c54a..58321112 100644 --- a/internal/wasm/binary/section_test.go +++ b/internal/wasm/binary/section_test.go @@ -77,7 +77,7 @@ func TestMemorySection(t *testing.T) { 0x01, // 1 memory 0x01, 0x02, 0x03, // (memory 2 3) }, - expected: &wasm.Memory{Min: 2, Max: &three}, + expected: &wasm.Memory{Min: 2, Max: three}, }, } @@ -85,7 +85,7 @@ func TestMemorySection(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - memories, err := decodeMemorySection(bytes.NewReader(tc.input)) + memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemoryMaxPages) require.NoError(t, err) require.Equal(t, tc.expected, memories) }) @@ -94,9 +94,10 @@ func TestMemorySection(t *testing.T) { func TestMemorySection_Errors(t *testing.T) { tests := []struct { - name string - input []byte - expectedErr string + name string + input []byte + memoryMaxPages uint32 + expectedErr string }{ { name: "min and min with max", @@ -112,8 +113,12 @@ func TestMemorySection_Errors(t *testing.T) { for _, tt := range tests { tc := tt + if tc.memoryMaxPages == 0 { + tc.memoryMaxPages = wasm.MemoryMaxPages + } + t.Run(tc.name, func(t *testing.T) { - _, err := decodeMemorySection(bytes.NewReader(tc.input)) + _, err := decodeMemorySection(bytes.NewReader(tc.input), tc.memoryMaxPages) require.EqualError(t, err, tc.expectedErr) }) } diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go index 9f62db30..b0fcc697 100644 --- a/internal/wasm/memory.go +++ b/internal/wasm/memory.go @@ -2,6 +2,7 @@ package internalwasm import ( "encoding/binary" + "fmt" "math" ) @@ -12,7 +13,7 @@ const ( MemoryPageSize = uint32(65536) // MemoryMaxPages is maximum number of pages defined (2^16). // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem - MemoryMaxPages = MemoryPageSize + MemoryMaxPages = uint32(65536) // MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize". MemoryPageSizeInBits = 16 ) @@ -23,9 +24,8 @@ const ( // 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 uint32 - Max *uint32 + Buffer []byte + Min, Max uint32 } // Size implements wasm.Memory Size @@ -152,14 +152,9 @@ func memoryBytesNumToPages(bytesNum uint64) (pages uint32) { func (m *MemoryInstance) Grow(newPages uint32) (result uint32) { currentPages := memoryBytesNumToPages(uint64(len(m.Buffer))) - maxPages := MemoryMaxPages - if m.Max != nil { - maxPages = *m.Max - } - // If exceeds the max of memory size, we push -1 according to the spec. - if currentPages+newPages > maxPages { - return 0xffffffff // = -1 in signed 32 bit integer. + if currentPages+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))...) @@ -171,3 +166,22 @@ func (m *MemoryInstance) Grow(newPages uint32) (result uint32) { func (m *MemoryInstance) PageSize() (result uint32) { return memoryBytesNumToPages(uint64(len(m.Buffer))) } + +// PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. Ex. 1 -> "64Ki" +// +// See https://www.w3.org/TR/wasm-core-1/#memory-instances%E2%91%A0 +func PagesToUnitOfBytes(pages uint32) string { + k := pages * 64 + if k < 1024 { + return fmt.Sprintf("%d Ki", k) + } + m := k / 1024 + if m < 1024 { + return fmt.Sprintf("%d Mi", m) + } + g := m / 1024 + if g < 1024 { + return fmt.Sprintf("%d Gi", g) + } + return fmt.Sprintf("%d Ti", g/1024) +} diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go index 50535d35..8036de26 100644 --- a/internal/wasm/memory_test.go +++ b/internal/wasm/memory_test.go @@ -9,8 +9,8 @@ import ( func TestMemoryPageConsts(t *testing.T) { require.Equal(t, MemoryPageSize, uint32(1)<= s.maximumFunctionTypes { + l := uint32(len(s.typeIDs)) + if l >= s.functionMaxTypes { return nil, fmt.Errorf("too many function types in a store") } id = FunctionTypeID(len(s.typeIDs)) diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index c8313cde..b0eaf01c 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -456,7 +456,7 @@ func TestStore_getTypeInstance(t *testing.T) { t.Run("too many functions", func(t *testing.T) { s := newStore() const max = 10 - s.maximumFunctionTypes = max + s.functionMaxTypes = max s.typeIDs = make(map[string]FunctionTypeID) for i := 0; i < max; i++ { s.typeIDs[strconv.Itoa(i)] = 0 @@ -662,12 +662,12 @@ func TestStore_resolveImports(t *testing.T) { t.Run("ok", func(t *testing.T) { s := newStore() max := uint32(10) - memoryInst := &MemoryInstance{Max: &max} + memoryInst := &MemoryInstance{Max: max} s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: { Type: ExternTypeMemory, Memory: memoryInst, }}, Name: moduleName} - _, _, _, memory, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: &Memory{Max: &max}}}}) + _, _, _, memory, err := s.resolveImports(&Module{ImportSection: []*Import{{Module: moduleName, Name: name, Type: ExternTypeMemory, DescMem: &Memory{Max: max}}}}) require.NoError(t, err) require.Equal(t, memory, memoryInst) }) @@ -684,13 +684,13 @@ func TestStore_resolveImports(t *testing.T) { t.Run("maximum size mismatch", func(t *testing.T) { s := newStore() max := uint32(10) - importMemoryType := &Memory{Max: &max} + importMemoryType := &Memory{Max: max} s.modules[moduleName] = &ModuleInstance{Exports: map[string]*ExportInstance{name: { Type: ExternTypeMemory, - Memory: &MemoryInstance{}, + Memory: &MemoryInstance{Max: MemoryMaxPages}, }}, 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, but actual has no max") + 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 516fda02..ee56f055 100644 --- a/internal/wasm/text/decoder.go +++ b/internal/wasm/text/decoder.go @@ -102,17 +102,17 @@ type moduleParser struct { // DecodeModule implements internalwasm.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) (result *wasm.Module, err error) { +func DecodeModule(source []byte, enabledFeatures wasm.Features, memoryMaxPages uint32) (result *wasm.Module, err error) { // TODO: when globals are supported, err on global vars if disabled // names are the wasm.Module NameSection // // * ModuleName: ex. "test" if (module $test) - // * FunctionNames: nil od no imported or module-defined function had a name - // * LocalNames: nil when no imported or module-defined function had named (param) fields. + // * FunctionNames: nil when neither imported nor module-defined functions had a name + // * 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) + p := newModuleParser(module, enabledFeatures, memoryMaxPages) p.source = source // A valid source must begin with the token '(', but it could be preceded by whitespace or comments. For this @@ -141,7 +141,7 @@ func DecodeModule(source []byte, enabledFeatures wasm.Features) (result *wasm.Mo return module, nil } -func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features) *moduleParser { +func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryMaxPages uint32) *moduleParser { p := moduleParser{module: module, enabledFeatures: enabledFeatures, typeNamespace: newIndexNamespace(module.SectionElementCount), funcNamespace: newIndexNamespace(module.SectionElementCount), @@ -150,7 +150,7 @@ func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features) *module p.typeParser = newTypeParser(p.typeNamespace, p.onTypeEnd) p.typeUseParser = newTypeUseParser(module, p.typeNamespace) p.funcParser = newFuncParser(enabledFeatures, p.typeUseParser, p.funcNamespace, p.endFunc) - p.memoryParser = newMemoryParser(p.memoryNamespace, p.endMemory) + p.memoryParser = newMemoryParser(uint32(memoryMaxPages), p.memoryNamespace, p.endMemory) return &p } @@ -449,7 +449,7 @@ func (p *moduleParser) endFunc(typeIdx wasm.Index, code *wasm.Code, name string, // endMemory adds the limits for the current memory, and increments memoryNamespace as it is shared across imported and // module-defined memories. Finally, this returns parseModule to prepare for the next field. -func (p *moduleParser) endMemory(min uint32, max *uint32) tokenParser { +func (p *moduleParser) endMemory(min, max uint32) tokenParser { p.module.MemorySection = &wasm.Memory{Min: min, Max: max} p.pos = positionModule return p.parseModule diff --git a/internal/wasm/text/decoder_test.go b/internal/wasm/text/decoder_test.go index 74e40cb1..a9ef204e 100644 --- a/internal/wasm/text/decoder_test.go +++ b/internal/wasm/text/decoder_test.go @@ -1170,14 +1170,14 @@ func TestDecodeModule(t *testing.T) { name: "memory", input: "(module (memory 1))", expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1}, + MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages}, }, }, { name: "memory ID", input: "(module (memory $mem 1))", expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1}, + MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages}, }, }, { @@ -1364,7 +1364,7 @@ func TestDecodeModule(t *testing.T) { (export "foo" (memory 0)) )`, expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 0}, + MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages}, ExportSection: map[string]*wasm.Export{ "foo": {Name: "foo", Type: wasm.ExternTypeMemory, Index: 0}, }, @@ -1377,7 +1377,7 @@ func TestDecodeModule(t *testing.T) { (memory 0) )`, expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 0}, + MemorySection: &wasm.Memory{Min: 0, Max: wasm.MemoryMaxPages}, ExportSection: map[string]*wasm.Export{ "foo": {Name: "foo", Type: wasm.ExternTypeMemory, Index: 0}, }, @@ -1409,7 +1409,7 @@ func TestDecodeModule(t *testing.T) { (export "memory" (memory $mem)) )`, expected: &wasm.Module{ - MemorySection: &wasm.Memory{Min: 1}, + MemorySection: &wasm.Memory{Min: 1, Max: wasm.MemoryMaxPages}, ExportSection: map[string]*wasm.Export{ "memory": {Name: "memory", Type: wasm.ExternTypeMemory, Index: 0}, }, @@ -1511,7 +1511,7 @@ func TestDecodeModule(t *testing.T) { if tc.enabledFeatures == 0 { tc.enabledFeatures = wasm.Features20191205 } - m, err := DecodeModule([]byte(tc.input), tc.enabledFeatures) + m, err := DecodeModule([]byte(tc.input), tc.enabledFeatures, wasm.MemoryMaxPages) require.NoError(t, err) require.Equal(t, tc.expected, m) }) @@ -1522,6 +1522,7 @@ func TestParseModule_Errors(t *testing.T) { tests := []struct { name, input string enabledFeatures wasm.Features + memoryMaxPages uint32 expectedErr string }{ { @@ -1967,6 +1968,12 @@ func TestParseModule_Errors(t *testing.T) { )`, expectedErr: "2:47: i64.extend16_s invalid as feature sign-extension-ops 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: "second memory", input: "(module (memory 1) (memory 1))", @@ -2112,14 +2119,17 @@ func TestParseModule_Errors(t *testing.T) { if tc.enabledFeatures == 0 { tc.enabledFeatures = wasm.Features20191205 } - _, err := DecodeModule([]byte(tc.input), tc.enabledFeatures) + if tc.memoryMaxPages == 0 { + tc.memoryMaxPages = wasm.MemoryMaxPages + } + _, err := DecodeModule([]byte(tc.input), tc.enabledFeatures, tc.memoryMaxPages) require.EqualError(t, err, tc.expectedErr) }) } } func TestModuleParser_ErrorContext(t *testing.T) { - p := newModuleParser(&wasm.Module{}, 0) + p := newModuleParser(&wasm.Module{}, 0, 0) tests := []struct { input string pos parserPosition diff --git a/internal/wasm/text/memory_parser.go b/internal/wasm/text/memory_parser.go index 574b5104..08428ae1 100644 --- a/internal/wasm/text/memory_parser.go +++ b/internal/wasm/text/memory_parser.go @@ -7,11 +7,11 @@ import ( wasm "github.com/tetratelabs/wazero/internal/wasm" ) -func newMemoryParser(memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser { - return &memoryParser{memoryNamespace: memoryNamespace, onMemory: onMemory} +func newMemoryParser(memoryMaxPages uint32, memoryNamespace *indexNamespace, onMemory onMemory) *memoryParser { + return &memoryParser{memoryMaxPages: memoryMaxPages, memoryNamespace: memoryNamespace, onMemory: onMemory} } -type onMemory func(min uint32, max *uint32) tokenParser +type onMemory func(min, max uint32) tokenParser // memoryParser parses a wasm.Memory from and dispatches to onMemory. // @@ -22,6 +22,9 @@ type onMemory func(min uint32, max *uint32) 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 internalwasm.Memory. + memoryMaxPages uint32 + memoryNamespace *indexNamespace // onMemory is invoked on end @@ -30,7 +33,7 @@ type memoryParser struct { // currentMin is reset on begin and read onMemory currentMin uint32 // currentMax is reset on begin and read onMemory - currentMax *uint32 + currentMax uint32 } // begin should be called after reaching the internalwasm.ExternTypeMemoryName keyword in a module field. Parsing @@ -46,7 +49,7 @@ type memoryParser struct { // calls beginMin --^ func (p *memoryParser) begin(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) { p.currentMin = 0 - p.currentMax = nil + p.currentMax = p.memoryMaxPages if tok == tokenID { // Ex. $mem if _, err := p.memoryNamespace.setID(tokenBytes); err != nil { return nil, err @@ -62,9 +65,10 @@ 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: - var overflow bool - if p.currentMin, overflow = decodeUint32(tokenBytes); overflow || p.currentMin > wasm.MemoryPageSize { - return nil, fmt.Errorf("min outside range of %d: %s", wasm.MemoryPageSize, tokenBytes) + 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)) + } else { + p.currentMin = i } return p.beginMax, nil case tokenRParen: @@ -80,12 +84,12 @@ func (p *memoryParser) beginMax(tok tokenType, tokenBytes []byte, line, col uint switch tok { case tokenUN: i, overflow := decodeUint32(tokenBytes) - if overflow || i > wasm.MemoryPageSize { - return nil, fmt.Errorf("min outside range of %d: %s", wasm.MemoryMaxPages, 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)) } else if i < p.currentMin { - return nil, fmt.Errorf("max %d < min %d", p.currentMax, p.currentMin) + return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", p.currentMin, wasm.PagesToUnitOfBytes(p.currentMin), i, wasm.PagesToUnitOfBytes(i)) } - p.currentMax = &i + p.currentMax = i return p.end, nil case tokenRParen: return p.end(tok, tokenBytes, line, col) diff --git a/internal/wasm/text/memory_parser_test.go b/internal/wasm/text/memory_parser_test.go index 10dece5a..2bdb7d9a 100644 --- a/internal/wasm/text/memory_parser_test.go +++ b/internal/wasm/text/memory_parser_test.go @@ -20,38 +20,38 @@ func TestMemoryParser(t *testing.T) { { name: "min 0", input: "(memory 0)", - expected: &wasm.Memory{}, + expected: &wasm.Memory{Max: max}, }, { name: "min 0, max 0", input: "(memory 0 0)", - expected: &wasm.Memory{Max: &zero}, + expected: &wasm.Memory{Max: zero}, }, { name: "min largest", input: "(memory 65536)", - expected: &wasm.Memory{Min: max}, + expected: &wasm.Memory{Min: max, Max: max}, }, { name: "min largest - ID", input: "(memory $mem 65536)", - expected: &wasm.Memory{Min: max}, + expected: &wasm.Memory{Min: max, Max: max}, expectedID: "mem", }, { name: "min 0, max largest", input: "(memory 0 65536)", - expected: &wasm.Memory{Max: &max}, + expected: &wasm.Memory{Max: max}, }, { name: "min largest max largest", input: "(memory 65536 65536)", - expected: &wasm.Memory{Min: max, Max: &max}, + expected: &wasm.Memory{Min: max, Max: max}, }, { name: "min largest max largest - ID", input: "(memory $mem 65536 65536)", - expected: &wasm.Memory{Min: max, Max: &max}, + expected: &wasm.Memory{Min: max, Max: max}, expectedID: "mem", }, } @@ -118,17 +118,17 @@ func TestMemoryParser_Errors(t *testing.T) { { name: "max < min", input: "(memory 1 0)", - expectedErr: "max 0 < min 1", + expectedErr: "min 1 pages (64 Ki) > max 0 pages (0 Ki)", }, { name: "min > limit", input: "(memory 4294967295)", - expectedErr: "min outside range of 65536: 4294967295", + expectedErr: "min 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)", }, { name: "max > limit", input: "(memory 0 4294967295)", - expectedErr: "min outside range of 65536: 4294967295", + expectedErr: "max 4294967295 pages (3 Ti) outside range of 65536 pages (4 Gi)", }, } @@ -163,11 +163,11 @@ func TestMemoryParser_Errors(t *testing.T) { func parseMemoryType(memoryNamespace *indexNamespace, input string) (*wasm.Memory, *memoryParser, error) { var parsed *wasm.Memory - var setFunc onMemory = func(min uint32, max *uint32) tokenParser { + var setFunc onMemory = func(min, max uint32) tokenParser { parsed = &wasm.Memory{Min: min, Max: max} return parseErr } - tp := newMemoryParser(memoryNamespace, setFunc) + tp := newMemoryParser(wasm.MemoryMaxPages, 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/tests/bench/memory_bench_test.go b/tests/bench/memory_bench_test.go index 1e3bf170..2069eee8 100644 --- a/tests/bench/memory_bench_test.go +++ b/tests/bench/memory_bench_test.go @@ -1,14 +1,13 @@ package bench import ( - "math" "testing" wasm "github.com/tetratelabs/wazero/internal/wasm" ) func BenchmarkMemory(b *testing.B) { - var mem = &wasm.MemoryInstance{Buffer: make([]byte, math.MaxUint16), Min: 1} + var mem = &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize), Min: 1} if !mem.WriteByte(10, 16) { b.Fail() } diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go index 9d1f8910..be4844c7 100644 --- a/tests/spectest/spec_test.go +++ b/tests/spectest/spec_test.go @@ -245,7 +245,7 @@ func addSpectestModule(t *testing.T, store *wasm.Store) { }, }, MemorySection: &wasm.Memory{ - Min: 1, Max: &memoryLimitMax, + Min: 1, Max: memoryLimitMax, }, TableSection: &wasm.Table{ Min: 10, Max: &tableLimitMax, @@ -319,8 +319,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) { case "module": buf, err := testcases.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - - mod, err := binary.DecodeModule(buf, wasm.Features20191205) + mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryMaxPages) require.NoError(t, err, msg) require.NoError(t, mod.Validate(wasm.Features20191205)) @@ -448,7 +447,7 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) { } func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg string) { - mod, err := binary.DecodeModule(buf, store.EnabledFeatures) + mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryMaxPages) if err != nil { return } diff --git a/vs/codec_test.go b/vs/codec_test.go index 1188e447..dd66afa5 100644 --- a/vs/codec_test.go +++ b/vs/codec_test.go @@ -59,7 +59,7 @@ func newExample() *wasm.Module { {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}}, }, - MemorySection: &wasm.Memory{Min: 1, Max: &three}, + MemorySection: &wasm.Memory{Min: 1, Max: three}, ExportSection: map[string]*wasm.Export{ "AddInt": {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)}, "": {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(3)}, @@ -93,13 +93,13 @@ func newExample() *wasm.Module { func TestExampleUpToDate(t *testing.T) { t.Run("binary.DecodeModule", func(t *testing.T) { - m, err := binary.DecodeModule(exampleBinary, enabledFeatures) + m, err := binary.DecodeModule(exampleBinary, enabledFeatures, wasm.MemoryMaxPages) require.NoError(t, err) require.Equal(t, example, m) }) t.Run("text.DecodeModule", func(t *testing.T) { - m, err := text.DecodeModule(exampleText, enabledFeatures) + m, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages) require.NoError(t, err) require.Equal(t, example, m) }) @@ -126,7 +126,7 @@ func BenchmarkCodecExample(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, enabledFeatures); err != nil { + if _, err := binary.DecodeModule(exampleBinary, enabledFeatures, wasm.MemoryMaxPages); err != nil { b.Fatal(err) } } @@ -140,7 +140,7 @@ func BenchmarkCodecExample(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, enabledFeatures); err != nil { + if _, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages); err != nil { b.Fatal(err) } } @@ -148,7 +148,7 @@ func BenchmarkCodecExample(b *testing.B) { b.Run("wat2wasm via text.DecodeModule->binary.EncodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if m, err := text.DecodeModule(exampleText, enabledFeatures); err != nil { + if m, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages); err != nil { b.Fatal(err) } else { _ = binary.EncodeModule(m) diff --git a/wasm.go b/wasm.go index fd415639..ece87f82 100644 --- a/wasm.go +++ b/wasm.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" internalwasm "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasm/binary" @@ -81,8 +82,9 @@ func NewRuntime() Runtime { func NewRuntimeWithConfig(config *RuntimeConfig) Runtime { return &runtime{ ctx: config.ctx, - store: internalwasm.NewStore(config.engine, config.enabledFeatures), + store: internalwasm.NewStore(config.newEngine(), config.enabledFeatures), enabledFeatures: config.enabledFeatures, + memoryMaxPages: config.memoryMaxPages, } } @@ -91,6 +93,7 @@ type runtime struct { ctx context.Context store *internalwasm.Store enabledFeatures internalwasm.Features + memoryMaxPages uint32 } // Module implements Runtime.Module @@ -116,7 +119,13 @@ func (r *runtime) CompileModule(source []byte) (*Module, error) { decoder = text.DecodeModule } - internal, err := decoder(source, r.enabledFeatures) + if r.memoryMaxPages > internalwasm.MemoryMaxPages { + return nil, fmt.Errorf("memoryMaxPages %d (%s) > specification max %d (%s)", + r.memoryMaxPages, internalwasm.PagesToUnitOfBytes(r.memoryMaxPages), + internalwasm.MemoryMaxPages, internalwasm.PagesToUnitOfBytes(internalwasm.MemoryMaxPages)) + } + + internal, err := decoder(source, r.enabledFeatures, r.memoryMaxPages) if err != nil { return nil, err } else if err = internal.Validate(r.enabledFeatures); err != nil { diff --git a/wasm_test.go b/wasm_test.go index 42bd52c2..2e3d828e 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -62,6 +62,7 @@ func TestRuntime_DecodeModule(t *testing.T) { func TestRuntime_DecodeModule_Errors(t *testing.T) { tests := []struct { name string + runtime Runtime source []byte expectedErr string }{ @@ -79,14 +80,36 @@ func TestRuntime_DecodeModule_Errors(t *testing.T) { source: []byte(`(modular)`), expectedErr: "1:2: unexpected field: modular", }, + { + name: "RuntimeConfig.memoryMaxPage too large", + runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(math.MaxUint32)), + source: []byte(`(module)`), + expectedErr: "memoryMaxPages 4294967295 (3 Ti) > specification max 65536 (4 Gi)", + }, + { + name: "memory has too many pages - text", + runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(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]", + }, + { + name: "memory has too many pages - binary", + runtime: NewRuntimeWithConfig(NewRuntimeConfig().WithMemoryMaxPages(2)), + source: binary.EncodeModule(&internalwasm.Module{MemorySection: &internalwasm.Memory{Min: 2, Max: 3}}), + expectedErr: "section memory: max 3 pages (192 Ki) outside range of 2 pages (128 Ki)", + }, } r := NewRuntime() for _, tt := range tests { tc := tt + if tc.runtime == nil { + tc.runtime = r + } + t.Run(tc.name, func(t *testing.T) { - _, err := r.CompileModule(tc.source) + _, err := tc.runtime.CompileModule(tc.source) require.EqualError(t, err, tc.expectedErr) }) }