Repository: tetratelabs/wazero (go)

Issue:
Add option to define maximum memory/cpu 
Currently, seems that any code can increase the memory without any soft-limit. That means that one WASM with `for { r = append(r, make([]byte, 10_000)...) }` will run and will take, eventually, all the memory available.

I would like to limit each runtime/wasm to have ~10MB, for example. Then the wasm must panic/crash due to "Out-Of-Memory" and the `grow` call must fail. 

_Also, would be interesting to limit the CPU usage, but I think it's hard to track the CPU usage, that could be more useful when WASM supports thread directly._

Pull request description:
Makes memory limit configurable and a compile error
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.

Fixes #417

Patch:
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/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/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/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/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/module.go b/internal/wasm/module.go
index 51197bba..eda52198 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -20,7 +20,7 @@ import (
 // * result is the module parsed or nil on error
 // * err is a FormatError invoking the parser, dangling block comments or unexpected characters.
 // See binary.DecodeModule and text.DecodeModule
-type DecodeModule func(source []byte, enabledFeatures Features) (result *Module, err error)
+type DecodeModule func(source []byte, enabledFeatures Features, memoryMaxPages uint32) (result *Module, err error)
 
 // EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
 // See binary.EncodeModule
@@ -586,7 +586,9 @@ type limitsType struct {
 }
 
 // Memory describes the limits of pages (64KB) in a memory.
-type Memory = limitsType
+type Memory struct {
+	Min, Max uint32
+}
 
 type GlobalType struct {
 	ValType ValueType
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index 8d01ed83..eac37cb7 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -41,9 +41,9 @@ type (
 		// do type-checks on indirect function calls.
 		typeIDs map[string]FunctionTypeID
 
-		//  maximumFunctionTypes represents the limit on the number of function types in a store.
+		// functionMaxTypes represents the limit on the number of function types in a store.
 		// Note: this is fixed to 2^27 but have this a field for testability.
-		maximumFunctionTypes int
+		functionMaxTypes uint32
 
 		// mux is used to guard the fields from concurrent access.
 		mux sync.RWMutex
@@ -244,12 +244,12 @@ func (m *ModuleInstance) getExport(name string, et ExternType) (*ExportInstance,
 
 func NewStore(engine Engine, enabledFeatures Features) *Store {
 	return &Store{
-		engine:               engine,
-		EnabledFeatures:      enabledFeatures,
-		moduleNames:          map[string]struct{}{},
-		modules:              map[string]*ModuleInstance{},
-		typeIDs:              map[string]FunctionTypeID{},
-		maximumFunctionTypes: maximumFunctionTypes,
+		engine:           engine,
+		EnabledFeatures:  enabledFeatures,
+		moduleNames:      map[string]struct{}{},
+		modules:          map[string]*ModuleInstance{},
+		typeIDs:          map[string]FunctionTypeID{},
+		functionMaxTypes: maximumFunctionTypes,
 	}
 }
 
@@ -453,15 +453,9 @@ func (s *Store) resolveImports(module *Module) (
 				return
 			}
 
-			if expected.Max != nil {
-				expectedMax := *expected.Max
-				if importedMemory.Max == nil {
-					err = errorNoMax(i, idx, expectedMax)
-					return
-				} else if expectedMax < *importedMemory.Max {
-					err = errorMaxSizeMismatch(i, idx, expectedMax, *importedMemory.Max)
-					return
-				}
+			if expected.Max < importedMemory.Max {
+				err = errorMaxSizeMismatch(i, idx, expected.Max, importedMemory.Max)
+				return
 			}
 		case ExternTypeGlobal:
 			expected := i.DescGlobal
@@ -549,8 +543,8 @@ func (s *Store) getTypeInstance(t *FunctionType) (*TypeInstance, error) {
 	key := t.String()
 	id, ok := s.typeIDs[key]
 	if !ok {
-		l := len(s.typeIDs)
-		if l >= 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/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/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/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 {


Test patch:
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_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/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_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_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)<<MemoryPageSizeInBits)
-	require.Equal(t, MemoryPageSize, MemoryMaxPages)
 	require.Equal(t, MemoryPageSize, uint32(1<<16))
+	require.Equal(t, MemoryMaxPages, uint32(1<<16))
 }
 
 func Test_MemoryPagesToBytesNum(t *testing.T) {
@@ -26,33 +26,23 @@ func Test_MemoryBytesNumToPages(t *testing.T) {
 }
 
 func TestMemoryInstance_Grow_Size(t *testing.T) {
-	t.Run("with max", func(t *testing.T) {
-		max := uint32(10)
-		m := &MemoryInstance{Max: &max, Buffer: make([]byte, 0)}
-		require.Equal(t, uint32(0), m.Grow(5))
-		require.Equal(t, uint32(5), m.PageSize())
-		// Zero page grow is well-defined, should return the current page correctly.
-		require.Equal(t, uint32(5), m.Grow(0))
-		require.Equal(t, uint32(5), m.PageSize())
-		require.Equal(t, uint32(5), m.Grow(4))
-		require.Equal(t, uint32(9), m.PageSize())
-		// At this point, the page size equal 9,
-		// so trying to grow two pages should result in failure.
-		require.Equal(t, int32(-1), int32(m.Grow(2)))
-		require.Equal(t, uint32(9), m.PageSize())
-		// But growing one page is still permitted.
-		require.Equal(t, uint32(9), m.Grow(1))
-		// Ensure that the current page size equals the max.
-		require.Equal(t, max, m.PageSize())
-	})
-	t.Run("without max", func(t *testing.T) {
-		m := &MemoryInstance{Buffer: make([]byte, 0)}
-		require.Equal(t, uint32(0), m.Grow(1))
-		require.Equal(t, uint32(1), m.PageSize())
-		// Trying to grow above MemoryMaxPages, the operation should fail.
-		require.Equal(t, int32(-1), int32(m.Grow(MemoryMaxPages)))
-		require.Equal(t, uint32(1), m.PageSize())
-	})
+	max := uint32(10)
+	m := &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
+	require.Equal(t, uint32(0), m.Grow(5))
+	require.Equal(t, uint32(5), m.PageSize())
+	// Zero page grow is well-defined, should return the current page correctly.
+	require.Equal(t, uint32(5), m.Grow(0))
+	require.Equal(t, uint32(5), m.PageSize())
+	require.Equal(t, uint32(5), m.Grow(4))
+	require.Equal(t, uint32(9), m.PageSize())
+	// At this point, the page size equal 9,
+	// so trying to grow two pages should result in failure.
+	require.Equal(t, int32(-1), int32(m.Grow(2)))
+	require.Equal(t, uint32(9), m.PageSize())
+	// But growing one page is still permitted.
+	require.Equal(t, uint32(9), m.Grow(1))
+	// Ensure that the current page size equals the max.
+	require.Equal(t, max, m.PageSize())
 }
 
 func TestReadByte(t *testing.T) {
@@ -89,6 +79,48 @@ func TestWriteUint32Le(t *testing.T) {
 	require.False(t, mem.WriteUint32Le(9, 16))
 }
 
+func TestPagesToUnitOfBytes(t *testing.T) {
+	tests := []struct {
+		name     string
+		pages    uint32
+		expected string
+	}{
+		{
+			name:     "zero",
+			pages:    0,
+			expected: "0 Ki",
+		},
+		{
+			name:     "one",
+			pages:    1,
+			expected: "64 Ki",
+		},
+		{
+			name:     "megs",
+			pages:    100,
+			expected: "6 Mi",
+		},
+		{
+			name:     "max memory",
+			pages:    MemoryMaxPages,
+			expected: "4 Gi",
+		},
+		{
+			name:     "max uint32",
+			pages:    math.MaxUint32,
+			expected: "3 Ti",
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			require.Equal(t, tc.expected, PagesToUnitOfBytes(tc.pages))
+		})
+	}
+}
+
 func TestMemoryInstance_HasSize(t *testing.T) {
 	memory := &MemoryInstance{Buffer: make([]byte, 100)}
 
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 676b6418..7e2f0bf8 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -141,9 +141,9 @@ func TestModule_allDeclarations(t *testing.T) {
 		// Memories.
 		{
 			module: &Module{
-				ImportSection: []*Import{{Type: ExternTypeMemory, DescMem: &limitsType{Min: 1}}},
+				ImportSection: []*Import{{Type: ExternTypeMemory, DescMem: &Memory{Min: 1, Max: 10}}},
 			},
-			expectedMemory: &Memory{Min: 1},
+			expectedMemory: &Memory{Min: 1, Max: 10},
 		},
 		{
 			module: &Module{
@@ -643,7 +643,7 @@ func TestModule_validateExports(t *testing.T) {
 			name:            "memory out of range",
 			enabledFeatures: Features20191205,
 			exportSection:   map[string]*Export{"e1": {Type: ExternTypeMemory, Index: 0}},
-			table:           &Memory{},
+			table:           &limitsType{},
 			expectedErr:     `memory for export["e1"] out of range`,
 		},
 	} {
@@ -720,9 +720,9 @@ 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, Max: max}}
 		mem := m.buildMemory()
 		require.Equal(t, min, mem.Min)
-		require.Equal(t, max, *mem.Max)
+		require.Equal(t, max, mem.Max)
 	})
 }
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_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_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_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)
 		})
 	}