Repository: tetratelabs/wazero
Pull request #352 (2022-03-09 09:15:49)

Adds global binary encoder and fixes scope of mutable-global flag
Before, we denied mutable globals when mutable-global=false, when really
that flag is about import/export. This fixes the flag so that it
disables externalizing a mutable global. Non-exported mutable globals
are common as implenented by compilers such as Emscripten.

See https://github.com/emscripten-core/emscripten/blob/main/system%2Flib%2Fcompiler-rt%2Fstack_ops.S



Patch:
diff --git a/config.go b/config.go
index 9a34bc6d..8bca03c2 100644
--- a/config.go
+++ b/config.go
@@ -54,7 +54,7 @@ func (r *RuntimeConfig) WithContext(ctx context.Context) *RuntimeConfig {
 // WithFeatureMutableGlobal allows globals to be mutable. This defaults to true as the feature was finished in
 // WebAssembly 1.0 (20191205).
 //
-// When false, a wasm.Global can never be cast to a wasm.MutableGlobal, and any source that includes mutable globals
+// 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 {
diff --git a/internal/wasi/fs.go b/internal/wasi/fs.go
index d257fc9c..95d0682c 100644
--- a/internal/wasi/fs.go
+++ b/internal/wasi/fs.go
@@ -1,7 +1,8 @@
 package internalwasi
 
 import (
-	"bytes"
+	"fmt"
+	"io"
 	"io/fs"
 	"os"
 	"runtime"
@@ -65,7 +66,7 @@ func (m *MemFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights,
 		}
 	}
 
-	ret := &memFile{buf: bytes.NewBuffer(buf)}
+	ret := &memFile{buf: buf}
 
 	if fsRights&wasi.R_FD_WRITE != 0 {
 		ret.flush = func(bts []byte) {
@@ -76,22 +77,51 @@ func (m *MemFS) OpenWASI(dirFlags uint32, path string, oFlags uint32, fsRights,
 	return ret, nil
 }
 
+// memFile implements wasi.File
 type memFile struct {
-	buf   *bytes.Buffer
-	flush func(bts []byte)
+	buf    []byte
+	offset int64
+	flush  func(bts []byte)
 }
 
+// Read implements io.Reader
 func (f *memFile) Read(p []byte) (int, error) {
-	return f.buf.Read(p)
+	// In memFile, the end of the buffer is the end of the file.
+	if f.offset == int64(len(f.buf)) {
+		return 0, io.EOF
+	}
+	nread := copy(p, f.buf[f.offset:])
+	f.offset += int64(nread)
+	return nread, nil
 }
 
+// Write implements io.Writer
 func (f *memFile) Write(p []byte) (int, error) {
-	return f.buf.Write(p)
+	nwritten := copy(f.buf[f.offset:], p)
+	f.buf = append(f.buf, p[nwritten:]...)
+	f.offset += int64(len(p))
+	return len(p), nil
+}
+
+// Seek implements io.Seeker
+func (f *memFile) Seek(offset int64, whence int) (int64, error) {
+	switch whence {
+	case io.SeekStart:
+		f.offset = offset
+	case io.SeekCurrent:
+		f.offset += offset
+	case io.SeekEnd:
+		f.offset = int64(len(f.buf)) + offset
+	default:
+		return 0, fmt.Errorf("invalid whence: %d", whence)
+	}
+	return f.offset, nil
 }
 
+// Close implements io.Closer
 func (f *memFile) Close() error {
 	if f.flush != nil {
-		f.flush(f.buf.Bytes())
+		f.flush(f.buf)
 	}
 	return nil
 }
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index 04b302c8..ec0169f8 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -741,7 +741,37 @@ type SnapshotPreview1 interface {
 	// FdRenumber is the WASI function named FunctionFdRenumber
 	FdRenumber(ctx wasm.Module, fd, to uint32) wasi.Errno
 
-	// FdSeek is the WASI function named FunctionFdSeek
+	// FdSeek is the WASI function to move the offset of a file descriptor.
+	//
+	// * fd: the file descriptor to move the offset of
+	// * offset: the signed int64, which is encoded as uint64, input argument to `whence`, which results in a new offset
+	// * whence: the operator that creates the new offset, given `offset` bytes
+	//   * If io.SeekStart, new offset == `offset`.
+	//   * If io.SeekCurrent, new offset == existing offset + `offset`.
+	//   * If io.SeekEnd, new offset == file size of `fd` + `offset`.
+	// * resultNewoffset: the offset in `ctx.Memory` to write the new offset to, relative to start of the file
+	//
+	// The wasi.Errno returned is wasi.ErrnoSuccess except the following error conditions:
+	// * wasi.ErrnoBadf - if `fd` is invalid
+	// * wasi.ErrnoFault - if `resultNewoffset` is an invalid offset in `ctx.Memory` due to the memory constraint
+	// * wasi.ErrnoInval - if `whence` is an invalid value
+	// * wasi.ErrnoIo - if other error happens during the operation of the underying file system
+	//
+	// For example, if fd 3 is a file with offset 0, and
+	//   parameters fd=3, offset=4, whence=0 (=io.SeekStart), resultNewOffset=1,
+	//   this function writes the below to `ctx.Memory`:
+	//
+	//                           uint64le
+	//                    +--------------------+
+	//                    |                    |
+	//          []byte{?, 4, 0, 0, 0, 0, 0, 0, 0, ? }
+	//  resultNewoffset --^
+	//
+	// See io.Seeker
+	// Note: ImportFdSeek shows this signature in the WebAssembly 1.0 (20191205) Text Format.
+	// Note: This is similar to `lseek` in POSIX.
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
+	// See https://linux.die.net/man/3/lseek
 	FdSeek(ctx wasm.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) wasi.Errno
 
 	// FdSync is the WASI function named FunctionFdSync
@@ -1218,7 +1248,24 @@ func (a *wasiAPI) FdRenumber(ctx wasm.Module, fd, to uint32) wasi.Errno {
 
 // FdSeek implements SnapshotPreview1.FdSeek
 func (a *wasiAPI) FdSeek(ctx wasm.Module, fd uint32, offset uint64, whence uint32, resultNewoffset uint32) wasi.Errno {
-	return wasi.ErrnoNosys // stubbed for GrainLang per #271
+	f, ok := a.opened[fd]
+	if !ok || f.file == nil {
+		return wasi.ErrnoBadf
+	}
+
+	if whence > io.SeekEnd /* exceeds the largest valid whence */ {
+		return wasi.ErrnoInval
+	}
+	newOffst, err := f.file.Seek(int64(offset), int(whence))
+	if err != nil {
+		return wasi.ErrnoIo
+	}
+
+	if !ctx.Memory().WriteUint32Le(resultNewoffset, uint32(newOffst)) {
+		return wasi.ErrnoFault
+	}
+
+	return wasi.ErrnoSuccess
 }
 
 // FdSync implements SnapshotPreview1.FdSync
diff --git a/internal/wasm/binary/encoder.go b/internal/wasm/binary/encoder.go
index a6810750..0b88b9c6 100644
--- a/internal/wasm/binary/encoder.go
+++ b/internal/wasm/binary/encoder.go
@@ -31,7 +31,7 @@ func EncodeModule(m *wasm.Module) (bytes []byte) {
 		bytes = append(bytes, encodeMemorySection(m.MemorySection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDGlobal) > 0 {
-		panic("TODO: GlobalSection")
+		bytes = append(bytes, encodeGlobalSection(m.GlobalSection)...)
 	}
 	if m.SectionElementCount(wasm.SectionIDExport) > 0 {
 		bytes = append(bytes, encodeExportSection(m.ExportSection)...)
diff --git a/internal/wasm/binary/export.go b/internal/wasm/binary/export.go
index 72a21dc7..caeed77a 100644
--- a/internal/wasm/binary/export.go
+++ b/internal/wasm/binary/export.go
@@ -32,7 +32,7 @@ func decodeExport(r *bytes.Reader) (i *wasm.Export, err error) {
 	return
 }
 
-// encodeExport returns the wasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format.
+// encodeExport returns the internalwasm.Export encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
 func encodeExport(i *wasm.Export) []byte {
diff --git a/internal/wasm/binary/global.go b/internal/wasm/binary/global.go
index 8adc1be1..bdcf3026 100644
--- a/internal/wasm/binary/global.go
+++ b/internal/wasm/binary/global.go
@@ -19,3 +19,16 @@ func decodeGlobal(r *bytes.Reader, features wasm.Features) (*wasm.Global, error)
 
 	return &wasm.Global{Type: gt, Init: init}, nil
 }
+
+// encodeGlobal returns the internalwasm.Global encoded in WebAssembly 1.0 (20191205) Binary Format.
+//
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
+func encodeGlobal(g *wasm.Global) []byte {
+	var mutable byte
+	if g.Type.Mutable {
+		mutable = 1
+	}
+	data := []byte{g.Type.ValType, mutable, g.Init.Opcode}
+	data = append(data, g.Init.Data...)
+	return append(data, wasm.OpcodeEnd)
+}
diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go
index ea92de5c..12679660 100644
--- a/internal/wasm/binary/import.go
+++ b/internal/wasm/binary/import.go
@@ -41,7 +41,7 @@ func decodeImport(r *bytes.Reader, idx uint32, features wasm.Features) (i *wasm.
 	return
 }
 
-// encodeImport returns the wasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format.
+// encodeImport returns the internalwasm.Import encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import
 func encodeImport(i *wasm.Import) []byte {
@@ -56,7 +56,12 @@ func encodeImport(i *wasm.Import) []byte {
 	case wasm.ExternTypeMemory:
 		panic("TODO: encodeExternTypeMemory")
 	case wasm.ExternTypeGlobal:
-		panic("TODO: encodeExternTypeGlobal")
+		g := i.DescGlobal
+		var mutable byte
+		if g.Mutable {
+			mutable = 1
+		}
+		data = append(data, g.ValType, mutable)
 	default:
 		panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type)))
 	}
diff --git a/internal/wasm/binary/limits.go b/internal/wasm/binary/limits.go
index 662ea6fd..0c5832d2 100644
--- a/internal/wasm/binary/limits.go
+++ b/internal/wasm/binary/limits.go
@@ -40,7 +40,7 @@ func decodeLimitsType(r *bytes.Reader) (*wasm.LimitsType, error) {
 	return ret, nil
 }
 
-// encodeLimitsType returns the wasm.LimitsType encoded in WebAssembly 1.0 (20191205) Binary Format.
+// encodeLimitsType returns the internalwasm.LimitsType encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6
 func encodeLimitsType(l *wasm.LimitsType) []byte {
diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go
index 2bbb0aa6..9d0e9860 100644
--- a/internal/wasm/binary/memory.go
+++ b/internal/wasm/binary/memory.go
@@ -28,7 +28,7 @@ func decodeMemoryType(r *bytes.Reader) (*wasm.MemoryType, error) {
 	return ret, nil
 }
 
-// encodeMemoryType returns the wasm.MemoryType encoded in WebAssembly 1.0 (20191205) Binary Format.
+// encodeMemoryType returns the internalwasm.MemoryType encoded in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory
 func encodeMemoryType(i *wasm.MemoryType) []byte {
diff --git a/internal/wasm/binary/names.go b/internal/wasm/binary/names.go
index 7a128b56..883188b3 100644
--- a/internal/wasm/binary/names.go
+++ b/internal/wasm/binary/names.go
@@ -150,7 +150,8 @@ func decodeFunctionCount(r *bytes.Reader, subsectionID uint8) (uint32, error) {
 	return functionCount, nil
 }
 
-// encodeNameSectionData serializes the data for the "name" key in SectionIDCustom according to the standard:
+// encodeNameSectionData serializes the data for the "name" key in internalwasm.SectionIDCustom according to the
+// standard:
 //
 // Note: The result can be nil because this does not encode empty subsections
 //
diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go
index eba34b92..e1ebae1c 100644
--- a/internal/wasm/binary/section.go
+++ b/internal/wasm/binary/section.go
@@ -215,7 +215,8 @@ func encodeSection(sectionID wasm.SectionID, contents []byte) []byte {
 	return append([]byte{sectionID}, encodeSizePrefixed(contents)...)
 }
 
-// encodeTypeSection encodes a SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary Format.
+// encodeTypeSection encodes a internalwasm.SectionIDType for the given imports in WebAssembly 1.0 (20191205) Binary
+// Format.
 //
 // See encodeFunctionType
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0
@@ -227,7 +228,8 @@ func encodeTypeSection(types []*wasm.FunctionType) []byte {
 	return encodeSection(wasm.SectionIDType, contents)
 }
 
-// encodeImportSection encodes a SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary Format.
+// encodeImportSection encodes a internalwasm.SectionIDImport for the given imports in WebAssembly 1.0 (20191205) Binary
+// Format.
 //
 // See encodeImport
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0
@@ -239,8 +241,8 @@ func encodeImportSection(imports []*wasm.Import) []byte {
 	return encodeSection(wasm.SectionIDImport, contents)
 }
 
-// encodeFunctionSection encodes a SectionIDFunction for the type indices associated with module-defined functions in
-// WebAssembly 1.0 (20191205) Binary Format.
+// encodeFunctionSection encodes a internalwasm.SectionIDFunction for the type indices associated with module-defined
+// functions in WebAssembly 1.0 (20191205) Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0
 func encodeFunctionSection(typeIndices []wasm.Index) []byte {
@@ -251,7 +253,8 @@ func encodeFunctionSection(typeIndices []wasm.Index) []byte {
 	return encodeSection(wasm.SectionIDFunction, contents)
 }
 
-// encodeCodeSection encodes a SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205) Binary Format.
+// encodeCodeSection encodes a internalwasm.SectionIDCode for the module-defined function in WebAssembly 1.0 (20191205)
+// Binary Format.
 //
 // See encodeCode
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0
@@ -263,7 +266,8 @@ func encodeCodeSection(code []*wasm.Code) []byte {
 	return encodeSection(wasm.SectionIDCode, contents)
 }
 
-// encodeMemorySection encodes a SectionIDMemory for the module-defined function in WebAssembly 1.0 (20191205) Binary Format.
+// encodeMemorySection encodes a internalwasm.SectionIDMemory for the module-defined function in WebAssembly 1.0
+// (20191205) Binary Format.
 //
 // See encodeMemoryType
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0
@@ -275,7 +279,21 @@ func encodeMemorySection(memories []*wasm.MemoryType) []byte {
 	return encodeSection(wasm.SectionIDMemory, contents)
 }
 
-// encodeExportSection encodes a SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary Format.
+// encodeGlobalSection encodes a internalwasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary
+// Format.
+//
+// See encodeGlobal
+// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0
+func encodeGlobalSection(globals []*wasm.Global) []byte {
+	contents := leb128.EncodeUint32(uint32(len(globals)))
+	for _, g := range globals {
+		contents = append(contents, encodeGlobal(g)...)
+	}
+	return encodeSection(wasm.SectionIDGlobal, contents)
+}
+
+// encodeExportSection encodes a internalwasm.SectionIDExport for the given exports in WebAssembly 1.0 (20191205) Binary
+// Format.
 //
 // See encodeExport
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0
@@ -287,7 +305,8 @@ func encodeExportSection(exports map[string]*wasm.Export) []byte {
 	return encodeSection(wasm.SectionIDExport, contents)
 }
 
-// encodeStartSection encodes a SectionIDStart for the given function index in WebAssembly 1.0 (20191205) Binary Format.
+// encodeStartSection encodes a internalwasm.SectionIDStart for the given function index in WebAssembly 1.0 (20191205)
+// Binary Format.
 //
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0
 func encodeStartSection(funcidx wasm.Index) []byte {
diff --git a/internal/wasm/binary/types.go b/internal/wasm/binary/types.go
index 474bbcaa..c25a59ac 100644
--- a/internal/wasm/binary/types.go
+++ b/internal/wasm/binary/types.go
@@ -44,11 +44,8 @@ func decodeGlobalType(r *bytes.Reader, features wasm.Features) (*wasm.GlobalType
 	}
 
 	switch mut := b; mut {
-	case 0x00:
-	case 0x01:
-		if err = features.Require(wasm.FeatureMutableGlobal); err != nil {
-			return nil, err
-		}
+	case 0x00: // not mutable
+	case 0x01: // mutable
 		ret.Mutable = true
 	default:
 		return nil, fmt.Errorf("%w for mutability: %#x != 0x00 or 0x01", ErrInvalidByte, mut)
diff --git a/internal/wasm/features.go b/internal/wasm/features.go
index 2d0877d0..5d537a10 100644
--- a/internal/wasm/features.go
+++ b/internal/wasm/features.go
@@ -16,9 +16,10 @@ type Features uint64
 const Features20191205 = FeatureMutableGlobal
 
 const (
-	// FeatureMutableGlobal decides if parsing should succeed on internalwasm.GlobalType Mutable
+	// FeatureMutableGlobal decides if global vars are allowed to be imported or exported (ExternTypeGlobal)
 	// See https://github.com/WebAssembly/mutable-global
 	FeatureMutableGlobal Features = 1 << iota
+
 	// FeatureSignExtensionOps decides if parsing should succeed on internalwasm.GlobalType Mutable
 	// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
 	FeatureSignExtensionOps
diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go
index eac65a90..4bd75e68 100644
--- a/internal/wasm/func_validation.go
+++ b/internal/wasm/func_validation.go
@@ -22,6 +22,7 @@ import (
 // Returns an error if the instruction sequence is not valid,
 // or potentially it can exceed the maximum number of values on the stack.
 func validateFunction(
+	enabledFeatures Features,
 	functionType *FunctionType,
 	body []byte,
 	localTypes []ValueType,
@@ -31,7 +32,6 @@ func validateFunction(
 	tables []*TableType,
 	types []*FunctionType,
 	maxStackValues int,
-	enabledFeatures Features,
 ) error {
 	// Note: In WebAssembly 1.0 (20191205), multiple memories are not allowed.
 	hasMemory := len(memories) > 0
@@ -337,7 +337,7 @@ func validateFunction(
 				if index >= uint32(len(globals)) {
 					return fmt.Errorf("invalid global index")
 				} else if !globals[index].Mutable {
-					return fmt.Errorf("globa.set on immutable global type")
+					return fmt.Errorf("global.set when not mutable")
 				} else if err := valueTypeStack.popAndVerifyType(
 					globals[index].ValType); err != nil {
 					return err
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 61e5f1f2..061987a1 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, features Features) (result *Module, err error)
+type DecodeModule func(source []byte, enabledFeatures Features) (result *Module, err error)
 
 // EncodeModule encodes the given module into a byte slice depending on the format of the implementation.
 // See binary.EncodeModule
@@ -37,7 +37,7 @@ type Module struct {
 	// TypeSection contains the unique FunctionType of functions imported or defined in this module.
 	//
 	// Note: Currently, there is no type ambiguity in the index as WebAssembly 1.0 only defines function type.
-	// In the future, other types may be introduced to support features such as module linking.
+	// In the future, other types may be introduced to support Features such as module linking.
 	//
 	// Note: In the Binary Format, this is SectionIDType.
 	//
@@ -212,7 +212,7 @@ func (m *Module) Validate(enabledFeatures Features) error {
 	}
 
 	if m.CodeSection != nil {
-		if err := m.validateFunctions(functions, globals, memories, tables, enabledFeatures); err != nil {
+		if err := m.validateFunctions(enabledFeatures, functions, globals, memories, tables); err != nil {
 			return err
 		}
 	} else {
@@ -221,7 +221,11 @@ func (m *Module) Validate(enabledFeatures Features) error {
 		}
 	}
 
-	if err := m.validateExports(functions, globals, memories, tables); err != nil {
+	if err := m.validateImports(enabledFeatures); err != nil {
+		return err
+	}
+
+	if err := m.validateExports(enabledFeatures, functions, globals, memories, tables); err != nil {
 		return err
 	}
 
@@ -260,7 +264,7 @@ func (m *Module) validateGlobals(globals []*GlobalType, maxGlobals int) error {
 	return nil
 }
 
-func (m *Module) validateFunctions(functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType, enabledFeatures Features) error {
+func (m *Module) validateFunctions(enabledFeatures Features, functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType) error {
 	functionCount := m.SectionElementCount(SectionIDFunction)
 	codeCount := m.SectionElementCount(SectionIDCode)
 	if functionCount == 0 && codeCount == 0 {
@@ -280,10 +284,11 @@ func (m *Module) validateFunctions(functions []Index, globals []*GlobalType, mem
 		}
 
 		if err := validateFunction(
+			enabledFeatures,
 			m.TypeSection[typeIndex],
 			m.CodeSection[idx].Body,
 			m.CodeSection[idx].LocalTypes,
-			functions, globals, memories, tables, m.TypeSection, maximumValuesOnStack, enabledFeatures); err != nil {
+			functions, globals, memories, tables, m.TypeSection, maximumValuesOnStack); err != nil {
 
 			return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err)
 		}
@@ -305,7 +310,7 @@ func (m *Module) funcDesc(sectionID SectionID, sectionIndex Index) string {
 		return fmt.Sprintf("%s[%d]", sectionIDName, sectionIndex)
 	}
 	sort.Strings(exportNames) // go map keys do not iterate consistently
-	return fmt.Sprintf("%s[%d] (export %s)", sectionIDName, sectionIndex, strings.Join(exportNames, ","))
+	return fmt.Sprintf("%s[%d] export[%s]", sectionIDName, sectionIndex, strings.Join(exportNames, ","))
 }
 
 func (m *Module) validateTables(tables []*TableType, globals []*GlobalType) error {
@@ -343,25 +348,46 @@ func (m *Module) validateMemories(memories []*MemoryType, globals []*GlobalType)
 	return nil
 }
 
-func (m *Module) validateExports(functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType) error {
+func (m *Module) validateImports(enabledFeatures Features) error {
+	for _, i := range m.ImportSection {
+		switch i.Type {
+		case ExternTypeGlobal:
+			if !i.DescGlobal.Mutable {
+				continue
+			}
+			if err := enabledFeatures.Require(FeatureMutableGlobal); err != nil {
+				return fmt.Errorf("invalid import[%q.%q] global: %w", i.Module, i.Name, err)
+			}
+		}
+	}
+	return nil
+}
+
+func (m *Module) validateExports(enabledFeatures Features, functions []Index, globals []*GlobalType, memories []*MemoryType, tables []*TableType) error {
 	for name, exp := range m.ExportSection {
 		index := exp.Index
 		switch exp.Type {
 		case ExternTypeFunc:
 			if index >= uint32(len(functions)) {
-				return fmt.Errorf("unknown function for export[%s]", name)
+				return fmt.Errorf("unknown function for export[%q]", name)
 			}
 		case ExternTypeGlobal:
 			if index >= uint32(len(globals)) {
-				return fmt.Errorf("unknown global for export[%s]", name)
+				return fmt.Errorf("unknown global for export[%q]", name)
+			}
+			if !globals[index].Mutable {
+				continue
+			}
+			if err := enabledFeatures.Require(FeatureMutableGlobal); err != nil {
+				return fmt.Errorf("invalid export[%q] global[%d]: %w", name, index, err)
 			}
 		case ExternTypeMemory:
 			if index != 0 || len(memories) == 0 {
-				return fmt.Errorf("unknown memory for export[%s]", name)
+				return fmt.Errorf("unknown memory for export[%q]", name)
 			}
 		case ExternTypeTable:
 			if index >= uint32(len(tables)) {
-				return fmt.Errorf("unknown table for export[%s]", name)
+				return fmt.Errorf("unknown table for export[%q]", name)
 			}
 		}
 	}
diff --git a/internal/wasm/text/decoder.go b/internal/wasm/text/decoder.go
index 08233810..cf9e98b7 100644
--- a/internal/wasm/text/decoder.go
+++ b/internal/wasm/text/decoder.go
@@ -103,7 +103,7 @@ 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) {
-	// TODO: when globals are supported, err on mutable globals if disabled
+	// TODO: when globals are supported, err on global vars if disabled
 
 	// names are the wasm.Module NameSection
 	//
diff --git a/wasi/wasi.go b/wasi/wasi.go
index 5b07c0b0..a72a70e3 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -1,7 +1,10 @@
 // Package wasi includes constants and interfaces used by both public and internal APIs.
 package wasi
 
-import "fmt"
+import (
+	"fmt"
+	"io"
+)
 
 const (
 	// ModuleSnapshotPreview1 is the module name WASI functions are exported into
@@ -28,10 +31,12 @@ const (
 	R_FD_WRITE
 )
 
+// File combines file I/O interfaces supported by ModuleSnapshotPreview1.
 type File interface {
-	Read([]byte) (int, error)
-	Write([]byte) (int, error)
-	Close() error
+	io.Reader
+	io.Writer
+	io.Seeker
+	io.Closer
 }
 
 // FS is an interface for a preopened directory.
diff --git a/wasm/wasm.go b/wasm/wasm.go
index ab9ee4b4..2763feb0 100644
--- a/wasm/wasm.go
+++ b/wasm/wasm.go
@@ -150,7 +150,7 @@ type Global interface {
 	Get() uint64
 }
 
-// MutableGlobal is a Global whose value can be updated at runtime.
+// MutableGlobal is a Global whose value can be updated at runtime (variable).
 type MutableGlobal interface {
 	Global
 


Test patch:
diff --git a/internal/wasi/fs_test.go b/internal/wasi/fs_test.go
new file mode 100644
index 00000000..53faa887
--- /dev/null
+++ b/internal/wasi/fs_test.go
@@ -0,0 +1,21 @@
+package internalwasi
+
+import (
+	"testing"
+	"testing/iotest"
+
+	"github.com/stretchr/testify/require"
+)
+
+// TestMemFile_Read_Seek tests the behavior of Read and Seek by iotest.TestReader.
+// See iotest.TestReader
+func TestMemFile_Read_Seek(t *testing.T) {
+	expectedFileContent := []byte("wazero") // arbitrary contents
+	memFile := &memFile{
+		buf: expectedFileContent,
+	}
+	// TestReader tests that io.Reader correctly reads the expected contents.
+	// It also tests io.Seeker when it's implemented, which memFile does.
+	err := iotest.TestReader(memFile, expectedFileContent)
+	require.NoError(t, err)
+}
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index 77ad01ae..8e129c88 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -1,7 +1,6 @@
 package internalwasi
 
 import (
-	"bytes"
 	"context"
 	_ "embed"
 	"encoding/binary"
@@ -1016,19 +1015,149 @@ func TestSnapshotPreview1_FdRenumber(t *testing.T) {
 	})
 }
 
-// TestSnapshotPreview1_FdSeek only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdSeek(t *testing.T) {
-	mod, fn := instantiateModule(t, FunctionFdSeek, ImportFdSeek, moduleName)
+	fd := uint32(3)              // arbitrary fd after 0, 1, and 2, that are stdin/out/err
+	resultNewoffset := uint32(1) // arbitrary offset in `ctx.Memory` for the new offset value
 
-	t.Run("SnapshotPreview1.FdSeek", func(t *testing.T) {
-		require.Equal(t, wasi.ErrnoNosys, NewAPI().FdSeek(mod, 0, 0, 0, 0))
+	var api *wasiAPI
+	mod, fn := instantiateModule(t, FunctionFdSeek, ImportFdSeek, moduleName, func(a *wasiAPI) {
+		api = a // for later tests
 	})
+	file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
+	api.opened[fd] = fileEntry{
+		path:    "test_path",
+		fileSys: memFS,
+		file:    file,
+	}
 
-	t.Run(FunctionFdSeek, func(t *testing.T) {
-		results, err := fn.Call(nil, 0, 0, 0, 0)
-		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoNosys, wasi.Errno(results[0])) // cast because results are always uint64
+	// TestSnapshotPreview1_FdSeek uses a matrix because setting up test files is complicated and has to be clean each time.
+	type fdSeekFn func(ctx publicwasm.Module, fd uint32, offset uint64, whence, resultNewOffset uint32) wasi.Errno
+	seekFns := []struct {
+		name   string
+		fdSeek func() fdSeekFn
+	}{
+		{"SnapshotPreview1.FdSeek", func() fdSeekFn {
+			return api.FdSeek
+		}},
+		{FunctionFdSeek, func() fdSeekFn {
+			return func(ctx publicwasm.Module, fd uint32, offset uint64, whence, resultNewoffset uint32) wasi.Errno {
+				ret, err := fn.Call(mod, uint64(fd), uint64(offset), uint64(whence), uint64(resultNewoffset))
+				require.NoError(t, err)
+				return wasi.Errno(ret[0])
+			}
+		}},
+	}
+
+	tests := []struct {
+		name           string
+		offset         int64
+		whence         int
+		expectedOffset int64
+		expectedMemory []byte
+	}{
+		{
+			name:           "SeekStart",
+			offset:         4, // arbitrary offset
+			whence:         io.SeekStart,
+			expectedOffset: 4, // = offset
+			expectedMemory: []byte{
+				'?',        // resultNewoffset is after this
+				4, 0, 0, 0, // = expectedOffset
+				'?',
+			},
+		},
+		{
+			name:           "SeekCurrent",
+			offset:         1, // arbitrary offset
+			whence:         io.SeekCurrent,
+			expectedOffset: 2, // = 1 (the initial offset of the test file) + 1 (offset)
+			expectedMemory: []byte{
+				'?',        // resultNewoffset is after this
+				2, 0, 0, 0, // = expectedOffset
+				'?',
+			},
+		},
+		{
+			name:           "SeekEnd",
+			offset:         -1, // arbitrary offset, note that offset can be negative
+			whence:         io.SeekEnd,
+			expectedOffset: 5, // = 6 (the size of the test file with content "wazero") + -1 (offset)
+			expectedMemory: []byte{
+				'?',        // resultNewoffset is after this
+				5, 0, 0, 0, // = expectedOffset
+				'?',
+			},
+		},
+	}
+
+	for _, seekFn := range seekFns {
+		sf := seekFn
+		t.Run(sf.name, func(t *testing.T) {
+			for _, tt := range tests {
+				tc := tt
+				t.Run(tc.name, func(t *testing.T) {
+					maskMemory(t, mod, len(tc.expectedMemory))
+					file.offset = 1 // set the initial offset of the file to 1
+
+					errno := sf.fdSeek()(mod, fd, uint64(tc.offset), uint32(tc.whence), resultNewoffset)
+					require.Equal(t, wasi.ErrnoSuccess, errno)
+
+					actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory)))
+					require.True(t, ok)
+					require.Equal(t, tc.expectedMemory, actual)
+
+					require.Equal(t, tc.expectedOffset, file.offset) // test that the offset of file is acutally updated.
+				})
+			}
+		})
+	}
+}
+
+func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
+	validFD := uint64(3)                                        // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
+	file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary valid file with non-empty contents
+	mod, fn := instantiateModule(t, FunctionFdSeek, ImportFdSeek, moduleName, func(a *wasiAPI) {
+		a.opened[uint32(validFD)] = fileEntry{
+			path:    "test_path",
+			fileSys: memFS,
+			file:    file,
+		}
 	})
+	memorySize := mod.Memory().Size()
+
+	tests := []struct {
+		name                                string
+		fd, offset, whence, resultNewoffset uint64
+		expectedErrno                       wasi.Errno
+	}{
+		{
+			name:          "invalid fd",
+			fd:            42, // arbitrary invalid fd
+			expectedErrno: wasi.ErrnoBadf,
+		},
+		{
+			name:          "invalid whence",
+			fd:            validFD,
+			whence:        3, // invalid whence, the largest whence io.SeekEnd(2) + 1
+			expectedErrno: wasi.ErrnoInval,
+		},
+		{
+			name:            "out-of-memory writing resultNewoffset",
+			fd:              validFD,
+			resultNewoffset: uint64(memorySize),
+			expectedErrno:   wasi.ErrnoFault,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			results, err := fn.Call(nil, tc.fd, tc.offset, tc.whence, tc.resultNewoffset)
+			require.NoError(t, err)
+			require.Equal(t, tc.expectedErrno, wasi.Errno(results[0])) // results[0] is the errno
+		})
+	}
+
 }
 
 // TestSnapshotPreview1_FdSync only tests it is stubbed for GrainLang per #271
@@ -1127,7 +1256,7 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 			actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 			require.True(t, ok)
 			require.Equal(t, expectedMemory, actual)
-			require.Equal(t, []byte("wazero"), file.buf.Bytes()) // verify the file was actually written
+			require.Equal(t, []byte("wazero"), file.buf) // verify the file was actually written
 		})
 	}
 }
@@ -1212,11 +1341,10 @@ func createFile(t *testing.T, path string, contents []byte) (*memFile, *MemFS) {
 	f, err := memFS.OpenWASI(0, path, wasi.O_CREATE|wasi.O_TRUNC, wasi.R_FD_WRITE, 0, 0)
 	require.NoError(t, err)
 
-	if _, err := io.Copy(f, bytes.NewBuffer(contents)); err != nil {
-		require.NoError(t, err)
-	}
+	memFile := f.(*memFile)
+	memFile.buf = append([]byte{}, contents...)
 
-	return f.(*memFile), memFS
+	return memFile, memFS
 }
 
 // TestSnapshotPreview1_PathCreateDirectory only tests it is stubbed for GrainLang per #271
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index 1b7fcdc5..b23d3293 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -1,7 +1,6 @@
 package binary
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -148,26 +147,6 @@ func TestDecodeModule_Errors(t *testing.T) {
 				subsectionIDModuleName, 0x02, 0x01, 'x'),
 			expectedErr: "section custom: redundant custom section name",
 		},
-		{
-			name:     fmt.Sprintf("define mutable global when %s disabled", wasm.FeatureMutableGlobal),
-			features: wasm.Features20191205.Set(wasm.FeatureMutableGlobal, false),
-			input: append(append(Magic, version...),
-				wasm.SectionIDGlobal, 0x06, // 6 bytes in this section
-				0x01, wasm.ValueTypeI32, 0x01, // 1 global i32 mutable
-				wasm.OpcodeI32Const, 0x00, wasm.OpcodeEnd, // arbitrary init to zero
-			),
-			expectedErr: "global[0]: feature mutable-global is disabled",
-		},
-		{
-			name:     fmt.Sprintf("import mutable global when %s disabled", wasm.FeatureMutableGlobal),
-			features: wasm.Features20191205.Set(wasm.FeatureMutableGlobal, false),
-			input: append(append(Magic, version...),
-				wasm.SectionIDImport, 0x08, // 8 bytes in this section
-				0x01, 0x01, 'a', 0x01, 'b', wasm.ExternTypeGlobal, // 1 import a.b of type global
-				wasm.ValueTypeI32, 0x01, // 1 global i32 mutable
-			),
-			expectedErr: "import[0] global[a.b]: feature mutable-global is disabled",
-		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/wasm/binary/encoder_test.go b/internal/wasm/binary/encoder_test.go
index 2c456946..0a7a6673 100644
--- a/internal/wasm/binary/encoder_test.go
+++ b/internal/wasm/binary/encoder_test.go
@@ -159,6 +159,29 @@ func TestModule_Encode(t *testing.T) {
 				0x01, 0x07, 'v', 'a', 'l', 'u', 'e', '_', '2', // index 1, size of "value_2", "value_2"
 			),
 		},
+		{
+			name: "exported global var",
+			input: &wasm.Module{
+				GlobalSection: []*wasm.Global{
+					{
+						Type: &wasm.GlobalType{ValType: i32, Mutable: true},
+						Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{0}},
+					},
+				},
+				ExportSection: map[string]*wasm.Export{
+					"sp": {Name: "sp", Type: wasm.ExternTypeGlobal, Index: wasm.Index(0)},
+				},
+			},
+			expected: append(append(Magic, version...),
+				wasm.SectionIDGlobal, 0x06, // 6 bytes in this section
+				0x01, wasm.ValueTypeI32, 0x01, // 1 global i32 mutable
+				wasm.OpcodeI32Const, 0x00, wasm.OpcodeEnd, // arbitrary init to zero
+				wasm.SectionIDExport, 0x06, // 6 bytes in this section
+				0x01,           // 1 export
+				0x02, 's', 'p', // size of "sp", "sp"
+				wasm.ExternTypeGlobal, 0x00, // global[0]
+			),
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/wasm/binary/export_test.go b/internal/wasm/binary/export_test.go
index 464eebf2..3d853de2 100644
--- a/internal/wasm/binary/export_test.go
+++ b/internal/wasm/binary/export_test.go
@@ -49,6 +49,41 @@ func TestEncodeExport(t *testing.T) {
 				0x0a,
 			},
 		},
+		{
+			name: "global no name, index 0",
+			input: &wasm.Export{ // Ex. (export "" (global 0)))
+				Type:  wasm.ExternTypeGlobal,
+				Name:  "",
+				Index: 0,
+			},
+			expected: []byte{0x00, wasm.ExternTypeGlobal, 0x00},
+		},
+		{
+			name: "global name, global index 0",
+			input: &wasm.Export{ // Ex. (export "pi" (global 0))
+				Type:  wasm.ExternTypeGlobal,
+				Name:  "pi",
+				Index: 0,
+			},
+			expected: []byte{
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				0x00,
+			},
+		},
+		{
+			name: "global name, index 10",
+			input: &wasm.Export{ // Ex. (export "pi" (global 10))
+				Type:  wasm.ExternTypeGlobal,
+				Name:  "pi",
+				Index: 10,
+			},
+			expected: []byte{
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				0x0a,
+			},
+		},
 		{
 			name: "memory no name, index 0",
 			input: &wasm.Export{ // Ex. (export "" (memory 0)))
diff --git a/internal/wasm/binary/global_test.go b/internal/wasm/binary/global_test.go
new file mode 100644
index 00000000..8a50d179
--- /dev/null
+++ b/internal/wasm/binary/global_test.go
@@ -0,0 +1,49 @@
+package binary
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	wasm "github.com/tetratelabs/wazero/internal/wasm"
+)
+
+func TestEncodeGlobal(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *wasm.Global
+		expected []byte
+	}{
+		{
+			name: "const",
+			input: &wasm.Global{
+				Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
+				Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{1}},
+			},
+			expected: []byte{
+				wasm.ValueTypeI32, 0x00, // 0 == const
+				wasm.OpcodeI32Const, 0x01, wasm.OpcodeEnd,
+			},
+		},
+		{
+			name: "var",
+			input: &wasm.Global{
+				Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+				Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: []byte{1}},
+			},
+			expected: []byte{
+				wasm.ValueTypeI32, 0x01, // 1 == var
+				wasm.OpcodeI32Const, 0x01, wasm.OpcodeEnd,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			bytes := encodeGlobal(tc.input)
+			require.Equal(t, tc.expected, bytes)
+		})
+	}
+}
diff --git a/internal/wasm/binary/import_test.go b/internal/wasm/binary/import_test.go
index aae519b2..69073438 100644
--- a/internal/wasm/binary/import_test.go
+++ b/internal/wasm/binary/import_test.go
@@ -69,6 +69,36 @@ func TestEncodeImport(t *testing.T) {
 				0x0a,
 			},
 		},
+		{
+			name: "global const",
+			input: &wasm.Import{
+				Type:       wasm.ExternTypeGlobal,
+				Module:     "math",
+				Name:       "pi",
+				DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				wasm.ValueTypeF64, 0x00, // 0 == const
+			},
+		},
+		{
+			name: "global var",
+			input: &wasm.Import{
+				Type:       wasm.ExternTypeGlobal,
+				Module:     "math",
+				Name:       "pi",
+				DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64, Mutable: true},
+			},
+			expected: []byte{
+				0x04, 'm', 'a', 't', 'h',
+				0x02, 'p', 'i',
+				wasm.ExternTypeGlobal,
+				wasm.ValueTypeF64, 0x01, // 1 == var
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go
index 8d7e9af5..8b945c7c 100644
--- a/internal/wasm/func_validation_test.go
+++ b/internal/wasm/func_validation_test.go
@@ -26,11 +26,11 @@ func TestValidateFunction_valueStackLimit(t *testing.T) {
 	body = append(body, OpcodeEnd)
 
 	t.Run("not exceed", func(t *testing.T) {
-		err := validateFunction(&FunctionType{}, body, nil, nil, nil, nil, nil, nil, max+1, Features20191205)
+		err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, max+1)
 		require.NoError(t, err)
 	})
 	t.Run("exceed", func(t *testing.T) {
-		err := validateFunction(&FunctionType{}, body, nil, nil, nil, nil, nil, nil, max, Features20191205)
+		err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, max)
 		require.Error(t, err)
 		expMsg := fmt.Sprintf("function may have %d stack values, which exceeds limit %d", valuesNum, max)
 		require.Equal(t, expMsg, err.Error())
@@ -69,7 +69,7 @@ func TestValidateFunction_SignExtensionOps(t *testing.T) {
 		tc := tt
 		t.Run(InstructionName(tc.input), func(t *testing.T) {
 			t.Run("disabled", func(t *testing.T) {
-				err := validateFunction(&FunctionType{}, []byte{tc.input}, nil, nil, nil, nil, nil, nil, maxStackHeight, Features20191205)
+				err := validateFunction(Features20191205, &FunctionType{}, []byte{tc.input}, nil, nil, nil, nil, nil, nil, maxStackHeight)
 				require.EqualError(t, err, tc.expectedErrOnDisable)
 			})
 			t.Run("enabled", func(t *testing.T) {
@@ -81,8 +81,7 @@ func TestValidateFunction_SignExtensionOps(t *testing.T) {
 					body = append(body, OpcodeI64Const)
 				}
 				body = append(body, tc.input, 123, OpcodeDrop, OpcodeEnd)
-				fmt.Println(body)
-				err := validateFunction(&FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight, FeatureSignExtensionOps)
+				err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight)
 				require.NoError(t, err)
 			})
 		})
diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go
index e1ee7ff4..119b5edc 100644
--- a/internal/wasm/host_test.go
+++ b/internal/wasm/host_test.go
@@ -186,7 +186,7 @@ func TestModule_validateHostFunctions(t *testing.T) {
 		}
 		err := m.validateHostFunctions()
 		require.Error(t, err)
-		require.EqualError(t, err, `host_function[0] (export "f1") is not a valid go func: kind != func: ptr`)
+		require.EqualError(t, err, `host_function[0] export["f1"] is not a valid go func: kind != func: ptr`)
 	})
 	t.Run("not a function  - exported after import", func(t *testing.T) {
 		m := Module{
@@ -198,7 +198,7 @@ func TestModule_validateHostFunctions(t *testing.T) {
 		}
 		err := m.validateHostFunctions()
 		require.Error(t, err)
-		require.EqualError(t, err, `host_function[0] (export "f1") is not a valid go func: kind != func: ptr`)
+		require.EqualError(t, err, `host_function[0] export["f1"] is not a valid go func: kind != func: ptr`)
 	})
 	t.Run("not a function - exported twice", func(t *testing.T) {
 		m := Module{
@@ -212,6 +212,6 @@ func TestModule_validateHostFunctions(t *testing.T) {
 		}
 		err := m.validateHostFunctions()
 		require.Error(t, err)
-		require.EqualError(t, err, `host_function[0] (export "f1","f2") is not a valid go func: kind != func: ptr`)
+		require.EqualError(t, err, `host_function[0] export["f1","f2"] is not a valid go func: kind != func: ptr`)
 	})
 }
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index 2bd077f8..8b4f57e3 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -352,7 +352,7 @@ func TestModule_validateGlobals(t *testing.T) {
 		m := Module{}
 		err := m.validateGlobals(make([]*GlobalType, 10), 9)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "too many globals")
+		require.EqualError(t, err, "too many globals in a module")
 	})
 	t.Run("global index out of range", func(t *testing.T) {
 		m := Module{GlobalSection: []*Global{
@@ -364,7 +364,7 @@ func TestModule_validateGlobals(t *testing.T) {
 		}}
 		err := m.validateGlobals(nil, 9)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "global index out of range")
+		require.EqualError(t, err, "global index out of range")
 	})
 	t.Run("invalid const expression", func(t *testing.T) {
 		m := Module{GlobalSection: []*Global{
@@ -375,7 +375,7 @@ func TestModule_validateGlobals(t *testing.T) {
 		}}
 		err := m.validateGlobals(nil, 9)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "invalid opcode for const expression")
+		require.EqualError(t, err, "invalid opcode for const expression: 0x0")
 	})
 	t.Run("ok", func(t *testing.T) {
 		m := Module{GlobalSection: []*Global{
@@ -414,7 +414,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []uint32{0},
 			CodeSection:     []*Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.NoError(t, err)
 	})
 	t.Run("function, but no code", func(t *testing.T) {
@@ -423,7 +423,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []Index{0},
 			CodeSection:     nil,
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
 		require.EqualError(t, err, "code count (0) != function count (1)")
 	})
@@ -433,7 +433,7 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []Index{1},
 			CodeSection:     []*Code{{Body: []byte{OpcodeEnd}}},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
 		require.EqualError(t, err, "invalid function[0]: type section index 1 out of range")
 	})
@@ -443,22 +443,22 @@ func TestModule_validateFunctions(t *testing.T) {
 			FunctionSection: []Index{0},
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
 		require.Contains(t, err.Error(), "invalid function[0]: cannot pop the 1st f32 operand")
 	})
-	t.Run("invalid - exported", func(t *testing.T) {
+	t.Run("in- exported", func(t *testing.T) {
 		m := Module{
 			TypeSection:     []*FunctionType{{}},
 			FunctionSection: []Index{0},
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 			ExportSection:   map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 0}},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), `invalid function[0] (export "f1"): cannot pop the 1st f32 operand`)
+		require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
 	})
-	t.Run("invalid - exported after import", func(t *testing.T) {
+	t.Run("in- exported after import", func(t *testing.T) {
 		m := Module{
 			TypeSection:     []*FunctionType{{}},
 			ImportSection:   []*Import{{Type: ExternTypeFunc}},
@@ -466,11 +466,11 @@ func TestModule_validateFunctions(t *testing.T) {
 			CodeSection:     []*Code{{Body: []byte{OpcodeF32Abs}}},
 			ExportSection:   map[string]*Export{"f1": {Name: "f1", Type: ExternTypeFunc, Index: 1}},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), `invalid function[0] (export "f1"): cannot pop the 1st f32 operand`)
+		require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`)
 	})
-	t.Run("invalid - exported twice", func(t *testing.T) {
+	t.Run("in- exported twice", func(t *testing.T) {
 		m := Module{
 			TypeSection:     []*FunctionType{{}},
 			FunctionSection: []Index{0},
@@ -480,9 +480,9 @@ func TestModule_validateFunctions(t *testing.T) {
 				"f2": {Name: "f2", Type: ExternTypeFunc, Index: 0},
 			},
 		}
-		err := m.validateFunctions(nil, nil, nil, nil, Features20191205)
+		err := m.validateFunctions(Features20191205, nil, nil, nil, nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), `invalid function[0] (export "f1","f2"): cannot pop the 1st f32 operand`)
+		require.Contains(t, err.Error(), `invalid function[0] export["f1","f2"]: cannot pop the 1st f32`)
 	})
 }
 
@@ -491,13 +491,13 @@ func TestModule_validateTables(t *testing.T) {
 		m := Module{}
 		err := m.validateTables(make([]*TableType, 10), nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "multiple tables are not supported")
+		require.EqualError(t, err, "multiple tables are not supported")
 	})
 	t.Run("table index out of range", func(t *testing.T) {
 		m := Module{ElementSection: []*ElementSegment{{TableIndex: 1000}}}
 		err := m.validateTables(make([]*TableType, 1), nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "table index out of range")
+		require.EqualError(t, err, "table index out of range")
 	})
 	t.Run("invalid const expr", func(t *testing.T) {
 		m := Module{ElementSection: []*ElementSegment{{
@@ -508,7 +508,7 @@ func TestModule_validateTables(t *testing.T) {
 		}}}
 		err := m.validateTables(make([]*TableType, 1), nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "invalid opcode for const expression: 0x0")
+		require.EqualError(t, err, "invalid const expression for element: invalid opcode for const expression: 0x0")
 	})
 	t.Run("ok", func(t *testing.T) {
 		m := Module{ElementSection: []*ElementSegment{{
@@ -528,7 +528,7 @@ func TestModule_validateMemories(t *testing.T) {
 		m := Module{}
 		err := m.validateMemories(make([]*LimitsType, 10), nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "multiple memories are not supported")
+		require.EqualError(t, err, "multiple memories are not supported")
 	})
 	t.Run("data section exits but memory not declared", func(t *testing.T) {
 		m := Module{DataSection: make([]*DataSegment, 1)}
@@ -540,7 +540,7 @@ func TestModule_validateMemories(t *testing.T) {
 		m := Module{DataSection: []*DataSegment{{MemoryIndex: 1}}}
 		err := m.validateMemories(make([]*LimitsType, 1), nil)
 		require.Error(t, err)
-		require.Contains(t, err.Error(), "memory index must be zero")
+		require.EqualError(t, err, "memory index must be zero")
 	})
 	t.Run("invalid const expr", func(t *testing.T) {
 		m := Module{DataSection: []*DataSegment{{
@@ -550,7 +550,7 @@ func TestModule_validateMemories(t *testing.T) {
 			},
 		}}}
 		err := m.validateMemories(make([]*LimitsType, 1), nil)
-		require.Contains(t, err.Error(), "invalid opcode for const expression: 0x0")
+		require.EqualError(t, err, "calculate offset: invalid opcode for const expression: 0x0")
 	})
 	t.Run("ok", func(t *testing.T) {
 		m := Module{DataSection: []*DataSegment{{
@@ -565,74 +565,158 @@ func TestModule_validateMemories(t *testing.T) {
 	})
 }
 
+func TestModule_validateImports(t *testing.T) {
+	for _, tc := range []struct {
+		name            string
+		enabledFeatures Features
+		i               *Import
+		expectedErr     string
+	}{
+		{name: "empty import section"},
+		{
+			name:            "func",
+			enabledFeatures: Features20191205,
+			i:               &Import{Module: "m", Name: "n", Type: ExternTypeFunc, DescFunc: 0},
+		},
+		{
+			name:            "global var disabled",
+			enabledFeatures: Features20191205.Set(FeatureMutableGlobal, false),
+			i: &Import{
+				Module:     "m",
+				Name:       "n",
+				Type:       ExternTypeGlobal,
+				DescGlobal: &GlobalType{ValType: ValueTypeI32, Mutable: true},
+			},
+			expectedErr: `invalid import["m"."n"] global: feature mutable-global is disabled`,
+		},
+		{
+			name:            "table",
+			enabledFeatures: Features20191205,
+			i: &Import{
+				Module:    "m",
+				Name:      "n",
+				Type:      ExternTypeTable,
+				DescTable: &TableType{ElemType: 0x70 /* funcref */, Limit: &LimitsType{Min: 1}},
+			},
+		},
+		{
+			name:            "memory",
+			enabledFeatures: Features20191205,
+			i: &Import{
+				Module:  "m",
+				Name:    "n",
+				Type:    ExternTypeMemory,
+				DescMem: &MemoryType{Min: 1},
+			},
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			m := Module{}
+			if tc.i != nil {
+				m.ImportSection = []*Import{tc.i}
+			}
+			err := m.validateImports(tc.enabledFeatures)
+			if tc.expectedErr != "" {
+				require.EqualError(t, err, tc.expectedErr)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
 func TestModule_validateExports(t *testing.T) {
 	for _, tc := range []struct {
-		name          string
-		exportSection map[string]*Export
-		functions     []Index
-		globals       []*GlobalType
-		memories      []*MemoryType
-		tables        []*TableType
-		expErr        bool
+		name            string
+		enabledFeatures Features
+		exportSection   map[string]*Export
+		functions       []Index
+		globals         []*GlobalType
+		memories        []*MemoryType
+		tables          []*TableType
+		expectedErr     string
 	}{
 		{name: "empty export section", exportSection: map[string]*Export{}},
 		{
-			name:          "valid func",
-			exportSection: map[string]*Export{"": {Type: ExternTypeFunc, Index: 0}},
-			functions:     []Index{100 /* arbitrary type id*/},
+			name:            "func",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeFunc, Index: 0}},
+			functions:       []Index{100 /* arbitrary type id*/},
+		},
+		{
+			name:            "func out of range",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeFunc, Index: 1}},
+			functions:       []Index{100 /* arbitrary type id*/},
+			expectedErr:     `unknown function for export["e1"]`,
 		},
 		{
-			name:          "invalid func",
-			exportSection: map[string]*Export{"": {Type: ExternTypeFunc, Index: 1}},
-			functions:     []Index{100 /* arbitrary type id*/},
-			expErr:        true,
+			name:            "global const",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeGlobal, Index: 0}},
+			globals:         []*GlobalType{{ValType: ValueTypeI32}},
 		},
 		{
-			name:          "valid global",
-			exportSection: map[string]*Export{"": {Type: ExternTypeGlobal, Index: 0}},
-			globals:       []*GlobalType{{}},
+			name:            "global var",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeGlobal, Index: 0}},
+			globals:         []*GlobalType{{ValType: ValueTypeI32, Mutable: true}},
 		},
 		{
-			name:          "invalid global",
-			exportSection: map[string]*Export{"": {Type: ExternTypeFunc, Index: 1}},
-			globals:       []*GlobalType{{}},
-			expErr:        true,
+			name:            "global var disabled",
+			enabledFeatures: Features20191205.Set(FeatureMutableGlobal, false),
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeGlobal, Index: 0}},
+			globals:         []*GlobalType{{ValType: ValueTypeI32, Mutable: true}},
+			expectedErr:     `invalid export["e1"] global[0]: feature mutable-global is disabled`,
 		},
 		{
-			name:          "valid table",
-			exportSection: map[string]*Export{"": {Type: ExternTypeTable, Index: 0}},
-			tables:        []*TableType{{}},
+			name:            "global out of range",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeGlobal, Index: 1}},
+			globals:         []*GlobalType{{}},
+			expectedErr:     `unknown global for export["e1"]`,
 		},
 		{
-			name:          "invalid table",
-			exportSection: map[string]*Export{"": {Type: ExternTypeTable, Index: 1}},
-			tables:        []*TableType{{}},
-			expErr:        true,
+			name:            "table",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeTable, Index: 0}},
+			tables:          []*TableType{{}},
 		},
 		{
-			name:          "valid memory",
-			exportSection: map[string]*Export{"": {Type: ExternTypeMemory, Index: 0}},
-			memories:      []*MemoryType{&LimitsType{}},
+			name:            "table out of range",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeTable, Index: 1}},
+			tables:          []*TableType{{}},
+			expectedErr:     `unknown table for export["e1"]`,
 		},
 		{
-			name:          "invalid memory index",
-			exportSection: map[string]*Export{"": {Type: ExternTypeMemory, Index: 1}},
-			expErr:        true,
+			name:            "memory",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeMemory, Index: 0}},
+			memories:        []*MemoryType{{}},
 		},
 		{
-			name:          "invalid memory index",
-			exportSection: map[string]*Export{"": {Type: ExternTypeMemory, Index: 1}},
+			name:            "memory out of range",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeMemory, Index: 0}},
+			expectedErr:     `unknown memory for export["e1"]`,
+		},
+		{
+			name:            "second memory unknown",
+			enabledFeatures: Features20191205,
+			exportSection:   map[string]*Export{"e1": {Type: ExternTypeMemory, Index: 1}},
 			// Multiple memories are not valid.
-			memories: []*MemoryType{&LimitsType{}, &LimitsType{}},
-			expErr:   true,
+			memories:    []*MemoryType{{}, {}},
+			expectedErr: `unknown memory for export["e1"]`,
 		},
 	} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			m := Module{ExportSection: tc.exportSection}
-			err := m.validateExports(tc.functions, tc.globals, tc.memories, tc.tables)
-			if tc.expErr {
-				require.Error(t, err)
+			err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memories, tc.tables)
+			if tc.expectedErr != "" {
+				require.EqualError(t, err, tc.expectedErr)
 			} else {
 				require.NoError(t, err)
 			}