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) }) }