Repository: tetratelabs/wazero
Pull request #401 (2022-03-21 05:36:38)

wasi: moves system resource management to the importing module
The prior design had a problem, where multiple imports of WASI would end
up having different file descriptors for the same file. Moreover, there
was no means to close any of these when a module importing WASI was
closed.

This moves all existing functionality to a new type SystemContext, which
is owned by the importing module, similar to how it owns its memory.
While this PR doesn't fix some problems such as unclosed files, the code
is now organized in a way it can be, and these issues will be resolved
by #394.

In order to fix scope, `WASISnapshotPreview1WithConfig` had to be
removed.

Signed-off-by: Adrian Cole <adrian@tetrate.io>


Patch:
diff --git a/README.md b/README.md
index 8e15ac8c..ee31afcd 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Here's an example of using wazero to invoke a factorial function:
 func main() {
 	// Read a WebAssembly binary containing an exported "fac" function.
 	// * Ex. (func (export "fac") (param i64) (result i64) ...
-	source, _ := os.ReadFile("./tests/engine/testdata/fac.wasm")
+	source, _ := os.ReadFile("./tests/bench/testdata/fac.wasm")
 
 	// Instantiate the module and return its exported functions
 	module, _ := wazero.NewRuntime().InstantiateModuleFromSource(source)
@@ -28,7 +28,7 @@ func main() {
 }
 ```
 
-Note: While the [source for this](tests/engine/testdata/fac.wat) is in the
+Note: While the [source for this](tests/bench/testdata/fac.wat) is in the
 WebAssembly 1.0 (20191205) Text Format, it could have been written in another
 language that compiles to (targets) WebAssembly, such as AssemblyScript, C, C++, Rust, TinyGo or Zig.
 
@@ -36,7 +36,7 @@ language that compiles to (targets) WebAssembly, such as AssemblyScript, C, C++,
 
 The former example is a pure function. While a good start, you probably are
 wondering how to do something more realistic, like read a file. WebAssembly
-Modules (Wasm) are sand-boxed similar to containers. They can't read anything
+Modules (Wasm) are sandboxed similar to containers. They can't read anything
 on your machine unless you explicitly allow it.
 
 System access is defined by an emerging specification called WebAssembly
@@ -93,7 +93,7 @@ wazero has two runtime modes: Interpreter and JIT. The only supported operating
 systems are ones we test, but that doesn't necessarily mean other operating
 system versions won't work.
 
-We currently test ubuntu-20.04, macos-11 and windows-2022 as packaged by
+We currently test Linux (Ubuntu and scratch), MacOS and Windows as packaged by
 [GitHub Actions](https://github.com/actions/virtual-environments).
 
 * Interpreter
@@ -104,11 +104,12 @@ We currently test ubuntu-20.04, macos-11 and windows-2022 as packaged by
   * MacOS and Windows are only tested on amd64.
 
 wazero has no dependencies and doesn't require CGO. This means it can also be
-embedded in an application that doesn't use an operating system. For example,
-we've tested every change via Docker `FROM scratch` to ensure that wazero doesn't
-require any OS dependency. It is an intended use case and a major
+embedded in an application that doesn't use an operating system. This is a main
 differentiator between wazero and alternatives.
 
+We verify wazero's independence by running tests in Docker's [scratch image](https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch).
+This approach ensures compatibility with any parent image.
+
 ## Standards Compliance
 
 The [WebAssembly Core Specification 1.0 (20191205)](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205)
diff --git a/internal/wasi/strings.go b/internal/cstring/cstring.go
similarity index 52%
rename from internal/wasi/strings.go
rename to internal/cstring/cstring.go
index 4f587bef..6792e94e 100644
--- a/internal/wasi/strings.go
+++ b/internal/cstring/cstring.go
@@ -1,27 +1,31 @@
-package internalwasi
+// Package cstring is named cstring because null-terminated strings are also known as CString and that avoids using
+// clashing package names like "strings" or a really long one like "null-terminated-strings"
+package cstring
 
 import (
 	"fmt"
 	"unicode/utf8"
 )
 
-// nullTerminatedStrings holds null-terminated strings. It ensures that
+// NullTerminatedStrings holds null-terminated strings. It ensures that
 // its length and total buffer size don't exceed the max of uint32.
-// nullTerminatedStrings are convenience struct for args_get and environ_get. (environ_get is not implemented yet)
+// NullTerminatedStrings are convenience struct for args_get and environ_get. (environ_get is not implemented yet)
 //
 // A Null-terminated string is a byte string with a NULL suffix ("\x00").
 // See https://en.wikipedia.org/wiki/Null-terminated_string
-type nullTerminatedStrings struct {
-	// nullTerminatedValues are null-terminated values with a NULL suffix.
-	nullTerminatedValues [][]byte
-	totalBufSize         uint32
+type NullTerminatedStrings struct {
+	// NullTerminatedValues are null-terminated values with a NULL suffix.
+	NullTerminatedValues [][]byte
+	TotalBufSize         uint32
 }
 
-// newNullTerminatedStrings creates a nullTerminatedStrings from the given string slice. It returns an error
-// if the length or the total buffer size of the result nullTerminatedStrings exceeds the maxBufSize
-func newNullTerminatedStrings(maxBufSize uint32, valName string, vals ...string) (*nullTerminatedStrings, error) {
+var EmptyNullTerminatedStrings = &NullTerminatedStrings{NullTerminatedValues: [][]byte{}}
+
+// NewNullTerminatedStrings creates a NullTerminatedStrings from the given string slice. It returns an error
+// if the length or the total buffer size of the result NullTerminatedStrings exceeds the maxBufSize
+func NewNullTerminatedStrings(maxBufSize uint32, valName string, vals ...string) (*NullTerminatedStrings, error) {
 	if len(vals) == 0 {
-		return &nullTerminatedStrings{nullTerminatedValues: [][]byte{}}, nil
+		return EmptyNullTerminatedStrings, nil
 	}
 	var strings [][]byte // don't pre-allocate as this function is size bound
 	totalBufSize := uint32(0)
@@ -37,5 +41,5 @@ func newNullTerminatedStrings(maxBufSize uint32, valName string, vals ...string)
 		totalBufSize = uint32(nextSize)
 		strings = append(strings, append([]byte(arg), 0))
 	}
-	return &nullTerminatedStrings{nullTerminatedValues: strings, totalBufSize: totalBufSize}, nil
+	return &NullTerminatedStrings{NullTerminatedValues: strings, TotalBufSize: totalBufSize}, nil
 }
diff --git a/internal/wasi/wasi.go b/internal/wasi/wasi.go
index 537f8ba7..77f1db1c 100644
--- a/internal/wasi/wasi.go
+++ b/internal/wasi/wasi.go
@@ -1,16 +1,13 @@
 package internalwasi
 
 import (
-	"context"
 	crand "crypto/rand"
 	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
 	"io/fs"
-	"math"
 	"os"
-	"strings"
 	"time"
 
 	internalwasm "github.com/tetratelabs/wazero/internal/wasm"
@@ -426,7 +423,7 @@ const (
 // See https://github.com/WebAssembly/WASI/issues/215
 // See https://wwa.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0.
 type SnapshotPreview1 interface {
-	// ArgsGet is the WASI function that reads command-line argument data (Args).
+	// ArgsGet is the WASI function that reads command-line argument data (WithArgs).
 	//
 	// There are two parameters. Both are offsets in wasm.Module Memory. If either are invalid due to
 	// memory constraints, this returns ErrnoFault.
@@ -454,7 +451,7 @@ type SnapshotPreview1 interface {
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
 	ArgsGet(ctx wasm.Module, argv, argvBuf uint32) wasi.Errno
 
-	// ArgsSizesGet is the WASI function named FunctionArgsSizesGet that reads command-line argument data (Args)
+	// ArgsSizesGet is the WASI function named FunctionArgsSizesGet that reads command-line argument data (WithArgs)
 	// sizes.
 	//
 	// There are two result parameters: these are offsets in the wasm.Module Memory to write
@@ -464,7 +461,7 @@ type SnapshotPreview1 interface {
 	// * resultArgc - is the offset to write the argument count to ctx.Memory
 	// * resultArgvBufSize - is the offset to write the null-terminated argument length to ctx.Memory
 	//
-	// For example, if Args are []string{"a","bc"} and
+	// For example, if WithArgs are []string{"a","bc"} and
 	//    parameters resultArgc=1 and resultArgvBufSize=6, this function writes the below to `ctx.Memory`:
 	//
 	//                   uint32le       uint32le
@@ -482,7 +479,7 @@ type SnapshotPreview1 interface {
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
 	ArgsSizesGet(ctx wasm.Module, resultArgc, resultArgvBufSize uint32) wasi.Errno
 
-	// EnvironGet is the WASI function named FunctionEnvironGet that reads environment variables. (Environ)
+	// EnvironGet is the WASI function named FunctionEnvironGet that reads environment variables. (WithEnviron)
 	//
 	// There are two parameters. Both are offsets in wasm.Module Memory. If either are invalid due to
 	// memory constraints, this returns ErrnoFault.
@@ -511,7 +508,7 @@ type SnapshotPreview1 interface {
 	EnvironGet(ctx wasm.Module, environ, environBuf uint32) wasi.Errno
 
 	// EnvironSizesGet is the WASI function named FunctionEnvironSizesGet that reads environment variable
-	// (Environ) sizes.
+	// (WithEnviron) sizes.
 	//
 	// There are two result parameters: these are offsets in the wasi.Module Memory to write
 	// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this
@@ -520,7 +517,7 @@ type SnapshotPreview1 interface {
 	// * resultEnvironc - is the offset to write the environment variable count to ctx.Memory
 	// * resultEnvironBufSize - is the offset to write the null-terminated environment variable length to ctx.Memory
 	//
-	// For example, if Environ is []string{"a=b","b=cd"} and
+	// For example, if WithEnviron is []string{"a=b","b=cd"} and
 	//    parameters resultEnvironc=1 and resultEnvironBufSize=6, this function writes the below to `ctx.Memory`:
 	//
 	//                   uint32le       uint32le
@@ -958,8 +955,6 @@ type SnapshotPreview1 interface {
 }
 
 type wasiAPI struct {
-	// cfg is the default configuration to use when there is no context.Context override (ConfigContextKey).
-	cfg *Config
 	// timeNowUnixNano is mutable for testing
 	timeNowUnixNano func() uint64
 	randSource      func([]byte) error
@@ -968,8 +963,8 @@ type wasiAPI struct {
 // SnapshotPreview1Functions returns all go functions that implement SnapshotPreview1.
 // These should be exported in the module named wasi.ModuleSnapshotPreview1.
 // See internalwasm.NewHostModule
-func SnapshotPreview1Functions(config *Config) (a *wasiAPI, nameToGoFunc map[string]interface{}) {
-	a = NewAPI(config)
+func SnapshotPreview1Functions() (a *wasiAPI, nameToGoFunc map[string]interface{}) {
+	a = NewAPI()
 	// Note: these are ordered per spec for consistency even if the resulting map can't guarantee that.
 	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#functions
 	nameToGoFunc = map[string]interface{}{
@@ -1024,9 +1019,9 @@ func SnapshotPreview1Functions(config *Config) (a *wasiAPI, nameToGoFunc map[str
 
 // ArgsGet implements SnapshotPreview1.ArgsGet
 func (a *wasiAPI) ArgsGet(ctx wasm.Module, argv, argvBuf uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	for _, arg := range cfg.args.nullTerminatedValues {
+	for _, arg := range sys.Args.NullTerminatedValues {
 		if !ctx.Memory().WriteUint32Le(argv, argvBuf) {
 			return wasi.ErrnoFault
 		}
@@ -1042,12 +1037,12 @@ func (a *wasiAPI) ArgsGet(ctx wasm.Module, argv, argvBuf uint32) wasi.Errno {
 
 // ArgsSizesGet implements SnapshotPreview1.ArgsSizesGet
 func (a *wasiAPI) ArgsSizesGet(ctx wasm.Module, resultArgc, resultArgvBufSize uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	if !ctx.Memory().WriteUint32Le(resultArgc, uint32(len(cfg.args.nullTerminatedValues))) {
+	if !ctx.Memory().WriteUint32Le(resultArgc, uint32(len(sys.Args.NullTerminatedValues))) {
 		return wasi.ErrnoFault
 	}
-	if !ctx.Memory().WriteUint32Le(resultArgvBufSize, cfg.args.totalBufSize) {
+	if !ctx.Memory().WriteUint32Le(resultArgvBufSize, sys.Args.TotalBufSize) {
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
@@ -1055,10 +1050,10 @@ func (a *wasiAPI) ArgsSizesGet(ctx wasm.Module, resultArgc, resultArgvBufSize ui
 
 // EnvironGet implements SnapshotPreview1.EnvironGet
 func (a *wasiAPI) EnvironGet(ctx wasm.Module, environ uint32, environBuf uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
 	// w.environ holds the environment variables in the form of "key=val\x00", so just copies it to the linear memory.
-	for _, env := range cfg.environ.nullTerminatedValues {
+	for _, env := range sys.Environ.NullTerminatedValues {
 		if !ctx.Memory().WriteUint32Le(environ, environBuf) {
 			return wasi.ErrnoFault
 		}
@@ -1074,12 +1069,12 @@ func (a *wasiAPI) EnvironGet(ctx wasm.Module, environ uint32, environBuf uint32)
 
 // EnvironSizesGet implements SnapshotPreview1.EnvironSizesGet
 func (a *wasiAPI) EnvironSizesGet(ctx wasm.Module, resultEnvironc uint32, resultEnvironBufSize uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	if !ctx.Memory().WriteUint32Le(resultEnvironc, uint32(len(cfg.environ.nullTerminatedValues))) {
+	if !ctx.Memory().WriteUint32Le(resultEnvironc, uint32(len(sys.Environ.NullTerminatedValues))) {
 		return wasi.ErrnoFault
 	}
-	if !ctx.Memory().WriteUint32Le(resultEnvironBufSize, cfg.environ.totalBufSize) {
+	if !ctx.Memory().WriteUint32Le(resultEnvironBufSize, sys.Environ.TotalBufSize) {
 		return wasi.ErrnoFault
 	}
 
@@ -1112,18 +1107,18 @@ func (a *wasiAPI) FdAllocate(ctx wasm.Module, fd uint32, offset, len uint64) was
 
 // FdClose implements SnapshotPreview1.FdClose
 func (a *wasiAPI) FdClose(ctx wasm.Module, fd uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	f, ok := cfg.opened[fd]
+	f, ok := sys.OpenedFiles[fd]
 	if !ok {
 		return wasi.ErrnoBadf
 	}
 
-	if f.file != nil {
-		f.file.Close()
+	if f.File != nil {
+		f.File.Close()
 	}
 
-	delete(cfg.opened, fd)
+	delete(sys.OpenedFiles, fd)
 
 	return wasi.ErrnoSuccess
 }
@@ -1136,9 +1131,9 @@ func (a *wasiAPI) FdDatasync(ctx wasm.Module, fd uint32) wasi.Errno {
 // FdFdstatGet implements SnapshotPreview1.FdFdstatGet
 // TODO: Currently FdFdstatget implements nothing except returning fake fs_right_inheriting
 func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	if _, ok := cfg.opened[fd]; !ok {
+	if _, ok := sys.OpenedFiles[fd]; !ok {
 		return wasi.ErrnoBadf
 	}
 	if !ctx.Memory().WriteUint64Le(resultStat+16, rightFDRead|rightFDWrite) {
@@ -1149,10 +1144,10 @@ func (a *wasiAPI) FdFdstatGet(ctx wasm.Module, fd uint32, resultStat uint32) was
 
 // FdPrestatGet implements SnapshotPreview1.FdPrestatGet
 func (a *wasiAPI) FdPrestatGet(ctx wasm.Module, fd uint32, resultPrestat uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	entry, ok := cfg.opened[fd]
-	if !ok || entry.path == "" {
+	entry, ok := sys.OpenedFiles[fd]
+	if !ok || entry.Path == "" {
 		return wasi.ErrnoBadf
 	}
 
@@ -1161,7 +1156,7 @@ func (a *wasiAPI) FdPrestatGet(ctx wasm.Module, fd uint32, resultPrestat uint32)
 		return wasi.ErrnoFault
 	}
 	// Write the length of the directory name at offset 4.
-	if !ctx.Memory().WriteUint32Le(resultPrestat+4, uint32(len(entry.path))) {
+	if !ctx.Memory().WriteUint32Le(resultPrestat+4, uint32(len(entry.Path))) {
 		return wasi.ErrnoFault
 	}
 
@@ -1200,20 +1195,20 @@ func (a *wasiAPI) FdPread(ctx wasm.Module, fd, iovs, iovsCount uint32, offset ui
 
 // FdPrestatDirName implements SnapshotPreview1.FdPrestatDirName
 func (a *wasiAPI) FdPrestatDirName(ctx wasm.Module, fd uint32, pathPtr uint32, pathLen uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	f, ok := cfg.opened[fd]
+	f, ok := sys.OpenedFiles[fd]
 	if !ok {
 		return wasi.ErrnoBadf
 	}
 
 	// Some runtimes may have another semantics. See internal/wasi/RATIONALE.md
-	if uint32(len(f.path)) < pathLen {
+	if uint32(len(f.Path)) < pathLen {
 		return wasi.ErrnoNametoolong
 	}
 
 	// TODO: FdPrestatDirName may have to return ErrnoNotdir if the type of the prestat data of `fd` is not a PrestatDir.
-	if !ctx.Memory().Write(pathPtr, []byte(f.path)[:pathLen]) {
+	if !ctx.Memory().Write(pathPtr, []byte(f.Path)[:pathLen]) {
 		return wasi.ErrnoFault
 	}
 	return wasi.ErrnoSuccess
@@ -1226,19 +1221,23 @@ func (a *wasiAPI) FdPwrite(ctx wasm.Module, fd, iovs, iovsCount uint32, offset u
 
 // FdRead implements SnapshotPreview1.FdRead
 func (a *wasiAPI) FdRead(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
 	var reader io.Reader
 
 	switch fd {
 	case 0:
-		reader = cfg.stdin
+		if sys.Stdin != nil {
+			reader = sys.Stdin
+		} else {
+			return wasi.ErrnoBadf
+		}
 	default:
-		f, ok := cfg.opened[fd]
-		if !ok || f.file == nil {
+		f, ok := sys.OpenedFiles[fd]
+		if !ok || f.File == nil {
 			return wasi.ErrnoBadf
 		}
-		reader = f.file
+		reader = f.File
 	}
 
 	var nread uint32
@@ -1282,13 +1281,13 @@ 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 {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	f, ok := cfg.opened[fd]
-	if !ok || f.file == nil {
+	f, ok := sys.OpenedFiles[fd]
+	if !ok || f.File == nil {
 		return wasi.ErrnoBadf
 	}
-	seeker, ok := f.file.(io.Seeker)
+	seeker, ok := f.File.(io.Seeker)
 	if !ok {
 		return wasi.ErrnoBadf
 	}
@@ -1320,21 +1319,21 @@ func (a *wasiAPI) FdTell(ctx wasm.Module, fd, resultOffset uint32) wasi.Errno {
 
 // FdWrite implements SnapshotPreview1.FdWrite
 func (a *wasiAPI) FdWrite(ctx wasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
 	var writer io.Writer
 
 	switch fd {
 	case 1:
-		writer = cfg.stdout
+		writer = sys.Stdout
 	case 2:
-		writer = cfg.stderr
+		writer = sys.Stderr
 	default:
-		f, ok := cfg.opened[fd]
-		if !ok || f.file == nil {
+		f, ok := sys.OpenedFiles[fd]
+		if !ok || f.File == nil {
 			return wasi.ErrnoBadf
 		}
-		writer = f.file
+		writer = f.File
 	}
 
 	var nwritten uint32
@@ -1421,10 +1420,10 @@ func posixOpenFlags(oFlags uint32, fsRights uint64) (pFlags int) {
 // PathOpen implements SnapshotPreview1.PathOpen
 func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, oflags uint32, fsRightsBase,
 	fsRightsInheriting uint64, fdflags, resultOpenedFd uint32) (errno wasi.Errno) {
-	cfg := a.config(ctx.Context())
+	sys := systemContext(ctx)
 
-	dir, ok := cfg.opened[fd]
-	if !ok || dir.fileSys == nil {
+	dir, ok := sys.OpenedFiles[fd]
+	if !ok || dir.FS == nil {
 		return wasi.ErrnoBadf
 	}
 
@@ -1433,7 +1432,7 @@ func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, ofla
 		return wasi.ErrnoFault
 	}
 	pathName := string(b)
-	f, err := dir.fileSys.OpenWASI(dirflags, pathName, oflags, fsRightsBase, fsRightsInheriting, fdflags)
+	f, err := dir.FS.OpenWASI(dirflags, pathName, oflags, fsRightsBase, fsRightsInheriting, fdflags)
 	if err != nil {
 		switch {
 		case errors.Is(err, fs.ErrNotExist):
@@ -1448,16 +1447,12 @@ func (a *wasiAPI) PathOpen(ctx wasm.Module, fd, dirflags, pathPtr, pathLen, ofla
 	// when ofagDir is set, the opened file must be a directory.
 	// TODO if oflags&oflagDir != 0 return wasi.ErrnoNotdir if stat != dir
 
-	newFD, err := a.randUnusedFD(cfg)
+	newFD, err := a.randUnusedFD(sys)
 	if err != nil {
 		return wasi.ErrnoIo
 	}
 
-	cfg.opened[newFD] = fileEntry{
-		path:    pathName,
-		fileSys: dir.fileSys,
-		file:    f,
-	}
+	sys.OpenedFiles[newFD] = &internalwasm.FileEntry{Path: pathName, File: f, FS: dir.FS}
 
 	if !ctx.Memory().WriteUint32Le(resultOpenedFd, newFD) {
 		return wasi.ErrnoFault
@@ -1543,96 +1538,9 @@ func (a *wasiAPI) SockShutdown(ctx wasm.Module, fd, how uint32) wasi.Errno {
 	return wasi.ErrnoNosys // stubbed for GrainLang per #271
 }
 
-type fileEntry struct {
-	path    string
-	fileSys wasi.FS
-	file    wasi.File
-}
-
-// ConfigContextKey indicates a context.Context includes an overriding Config.
-type ConfigContextKey struct{}
-
-type Config struct {
-	args *nullTerminatedStrings
-	// environ stores each environment variable in the form of "key=value",
-	// which is both convenient for the implementation of environ_get and matches os.Environ
-	environ *nullTerminatedStrings
-	stdin   io.Reader
-	stdout,
-	stderr io.Writer
-	opened map[uint32]fileEntry
-}
-
-func (c *Config) Stdin(reader io.Reader) {
-	c.stdin = reader
-}
-
-func (c *Config) Stdout(writer io.Writer) {
-	c.stdout = writer
-}
-
-func (c *Config) Stderr(writer io.Writer) {
-	c.stderr = writer
-}
-
-// Args returns an option to give a command-line arguments in SnapshotPreview1 or errs if the inputs are too large.
-//
-// Note: The only reason to set this is to control what's written by SnapshotPreview1.ArgsSizesGet and SnapshotPreview1.ArgsGet
-// Note: While similar in structure to os.Args, this controls what's visible in Wasm (ex the WASI function "_start").
-func (c *Config) Args(args ...string) error {
-	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it
-	if err != nil {
-		return err
-	}
-	c.args = wasiStrings
-	return nil
-}
-
-// Environ returns an option to set environment variables in SnapshotPreview1.
-// Environ returns an error if the input contains a string not joined with `=`, or if the inputs are too large.
-//  * environ: environment variables in the same format as that of `os.Environ`, where key/value pairs are joined with `=`.
-// See os.Environ
-//
-// Note: Implicit environment variable propagation into WASI is intentionally not done.
-// Note: The only reason to set this is to control what's written by SnapshotPreview1.EnvironSizesGet and SnapshotPreview1.EnvironGet
-// Note: While similar in structure to os.Environ, this controls what's visible in Wasm (ex the WASI function "_start").
-func (c *Config) Environ(environ ...string) error {
-	for i, env := range environ {
-		if !strings.Contains(env, "=") {
-			return fmt.Errorf("environ[%d] is not joined with '='", i)
-		}
-	}
-	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "environ", environ...) // TODO: this is crazy high even if spec allows it
-	if err != nil {
-		return err
-	}
-	c.environ = wasiStrings
-	return nil
-}
-
-func (c *Config) Preopen(dir string, fileSys wasi.FS) {
-	c.opened[uint32(len(c.opened))+3] = fileEntry{
-		path:    dir,
-		fileSys: fileSys,
-	}
-}
-
-// NewConfig sets configuration defaults
-func NewConfig() *Config {
-	return &Config{
-		args:    &nullTerminatedStrings{},
-		environ: &nullTerminatedStrings{},
-		stdin:   os.Stdin,
-		stdout:  os.Stdout,
-		stderr:  os.Stderr,
-		opened:  map[uint32]fileEntry{},
-	}
-}
-
 // NewAPI is exported for benchmarks
-func NewAPI(config *Config) *wasiAPI {
+func NewAPI() *wasiAPI {
 	return &wasiAPI{
-		cfg: config,
 		timeNowUnixNano: func() uint64 {
 			return uint64(time.Now().UnixNano())
 		},
@@ -1643,15 +1551,17 @@ func NewAPI(config *Config) *wasiAPI {
 	}
 }
 
-// config returns a potentially overridden Config.
-func (a *wasiAPI) config(ctx context.Context) *Config {
-	if cfg := ctx.Value(ConfigContextKey{}); cfg != nil {
-		return cfg.(*Config)
+func systemContext(ctx wasm.Module) *internalwasm.SystemContext {
+	if internal, ok := ctx.(*internalwasm.ModuleContext); !ok {
+		panic(fmt.Errorf("unsupported wasm.Module implementation: %v", ctx))
+	} else {
+		return internal.System
 	}
-	return a.cfg
 }
 
-func (a *wasiAPI) randUnusedFD(config *Config) (uint32, error) {
+func (a *wasiAPI) randUnusedFD(sys *internalwasm.SystemContext) (uint32, error) {
+	// TODO: consider not using random source here, as there's collision avoidance below anyway, and using the random
+	// source is both expensive (including allocation and decoding) and can deplete it.
 	rand := make([]byte, 4)
 	err := a.randSource(rand)
 	if err != nil {
@@ -1659,8 +1569,8 @@ func (a *wasiAPI) randUnusedFD(config *Config) (uint32, error) {
 	}
 	// fd is actually a signed int32, and must be a positive number.
 	fd := binary.LittleEndian.Uint32(rand) % (1 << 31)
-	for {
-		if _, ok := config.opened[fd]; !ok {
+	for { // avoid collisions on an already used file-descriptor. Note: this is not goroutine safe.
+		if _, ok := sys.OpenedFiles[fd]; !ok {
 			return fd, nil
 		}
 		fd = (fd + 1) % (1 << 31)
diff --git a/internal/wasm/module_context.go b/internal/wasm/module_context.go
index 7fd48001..a7969542 100644
--- a/internal/wasm/module_context.go
+++ b/internal/wasm/module_context.go
@@ -11,12 +11,7 @@ import (
 var _ publicwasm.Module = &ModuleContext{}
 
 func NewModuleContext(ctx context.Context, store *Store, instance *ModuleInstance) *ModuleContext {
-	return &ModuleContext{
-		ctx:    ctx,
-		memory: instance.Memory,
-		module: instance,
-		store:  store,
-	}
+	return &ModuleContext{ctx: ctx, memory: instance.Memory, module: instance, store: store}
 }
 
 // ModuleContext implements wasm.Module
@@ -27,13 +22,18 @@ type ModuleContext struct {
 	// memory is returned by Memory and overridden WithMemory
 	memory publicwasm.Memory
 	store  *Store
+
+	// System is not exposed publicly. This is currently only used by internalwasi.
+	// Note: This is a part of ModuleContext so that scope is correct and Close is coherent.
+	// TODO: In #394 unexport this for System()
+	System *SystemContext
 }
 
 // WithMemory allows overriding memory without re-allocation when the result would be the same.
 func (m *ModuleContext) WithMemory(memory *MemoryInstance) *ModuleContext {
 	// only re-allocate if it will change the effective memory
 	if m.memory == nil || (memory != nil && memory.Max != nil && *memory.Max > 0 && memory != m.memory) {
-		return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx}
+		return &ModuleContext{module: m.module, memory: memory, ctx: m.ctx, System: m.System}
 	}
 	return m
 }
@@ -52,14 +52,19 @@ func (m *ModuleContext) Context() context.Context {
 func (m *ModuleContext) WithContext(ctx context.Context) publicwasm.Module {
 	// only re-allocate if it will change the effective context
 	if ctx != nil && ctx != m.ctx {
-		return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx}
+		return &ModuleContext{module: m.module, memory: m.memory, ctx: ctx, System: m.System}
 	}
 	return m
 }
 
 // Close implements io.Closer
-func (m *ModuleContext) Close() error {
-	return m.store.CloseModule(m.module.Name)
+// Note: When there are multiple errors, the error returned is the last one.
+func (m *ModuleContext) Close() (err error) {
+	err = m.store.CloseModule(m.module.Name)
+	if err2 := m.System.Close(); err2 != nil {
+		err = err2
+	}
+	return
 }
 
 // Memory implements wasm.Module Memory
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index abede19c..bd6228ca 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -260,7 +260,12 @@ func NewStore(engine Engine, enabledFeatures Features) *Store {
 //
 // Note: Module.Validate must be called prior to instantiation.
 func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (*ModuleContext, error) {
-	if err := s.requireModuleName(name); err != nil {
+	sys, err := NewSystemContext() // TODO: from config in #394
+	if err != nil {                // fail early
+		return nil, err
+	}
+
+	if err = s.requireModuleName(name); err != nil {
 		return nil, err
 	}
 
@@ -314,6 +319,7 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string) (*
 
 	// Build the default context for calls to this module.
 	m.Ctx = NewModuleContext(ctx, s, m)
+	m.Ctx.System = sys
 
 	// Execute the start function.
 	if module.StartSection != nil {
diff --git a/internal/wasm/sys.go b/internal/wasm/sys.go
new file mode 100644
index 00000000..d16aee0f
--- /dev/null
+++ b/internal/wasm/sys.go
@@ -0,0 +1,121 @@
+package internalwasm
+
+import (
+	"fmt"
+	"io"
+	"math"
+	"os"
+	"strings"
+
+	"github.com/tetratelabs/wazero/internal/cstring"
+	"github.com/tetratelabs/wazero/wasi"
+)
+
+// FileEntry temporarily uses wasi types until #394.
+//
+// Note: This does not introduce cycles because the types here are in the package "wasi" not "internalwasi".
+type FileEntry struct {
+	Path string
+	FS   wasi.FS
+	File wasi.File
+}
+
+// SystemContext holds module-scoped system resources currently only used by internalwasi.
+//
+// TODO: Most fields are mutable so that WASI config can overwrite fields when starting a command module. This can be
+// fixed in #394 by replacing wazero.WASIConfig with wazero.SystemConfig, defaulted by wazero.RuntimeConfig and
+// overridable with InstantiateModule.
+type SystemContext struct {
+	// WithArgs hold a possibly empty (cstring.EmptyNullTerminatedStrings) list of arguments similar to os.Args.
+	//
+	// TODO: document this better in #396
+	Args *cstring.NullTerminatedStrings
+
+	// WithArgs hold a possibly empty (cstring.EmptyNullTerminatedStrings) list of arguments key/value pairs, similar to
+	// os.Environ.
+	//
+	// TODO: document this better in #396
+	Environ *cstring.NullTerminatedStrings
+
+	// WithStdin defaults to os.Stdin.
+	//
+	// TODO: change default to read os.DevNull in #396
+	Stdin io.Reader
+
+	// WithStdout defaults to os.Stdout.
+	//
+	// TODO: change default to io.Discard in #396
+	Stdout io.Writer
+
+	// WithStderr defaults to os.Stderr.
+	//
+	// TODO: change default to io.Discard in #396
+	Stderr io.Writer
+
+	// OpenedFiles are a map of file descriptor numbers (starting at 3) to open files (or directories).
+	// TODO: This is unguarded, so not goroutine-safe!
+	OpenedFiles map[uint32]*FileEntry
+}
+
+func NewSystemContext() (*SystemContext, error) {
+	return &SystemContext{
+		Args:        cstring.EmptyNullTerminatedStrings,
+		Environ:     cstring.EmptyNullTerminatedStrings,
+		Stdin:       os.Stdin, // TODO: stop this default #396
+		Stdout:      os.Stdout,
+		Stderr:      os.Stderr,
+		OpenedFiles: map[uint32]*FileEntry{},
+	}, nil // TODO: once we open stdin from os.DevNull, this may raise an error (albeit unlikely).
+}
+
+// WithStdin the same as wazero.WASIConfig WithStdin
+func (c *SystemContext) WithStdin(reader io.Reader) {
+	c.Stdin = reader
+}
+
+// WithStdout the same as wazero.WASIConfig WithStdout
+func (c *SystemContext) WithStdout(writer io.Writer) {
+	c.Stdout = writer
+}
+
+// WithStderr the same as wazero.WASIConfig WithStderr
+func (c *SystemContext) WithStderr(writer io.Writer) {
+	c.Stderr = writer
+}
+
+// WithArgs is the same as wazero.WASIConfig WithArgs
+func (c *SystemContext) WithArgs(args ...string) error {
+	wasiStrings, err := cstring.NewNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it
+	if err != nil {
+		return err
+	}
+	c.Args = wasiStrings
+	return nil
+}
+
+// WithEnviron is the same as wazero.WASIConfig WithEnviron
+func (c *SystemContext) WithEnviron(environ ...string) error {
+	for i, env := range environ {
+		if !strings.Contains(env, "=") {
+			return fmt.Errorf("environ[%d] is not joined with '='", i)
+		}
+	}
+	wasiStrings, err := cstring.NewNullTerminatedStrings(math.MaxUint32, "environ", environ...) // TODO: this is crazy high even if spec allows it
+	if err != nil {
+		return err
+	}
+	c.Environ = wasiStrings
+	return nil
+}
+
+// WithPreopen adds one element in  wazero.WASIConfig WithPreopens
+func (c *SystemContext) WithPreopen(dir string, fileSys wasi.FS) {
+	c.OpenedFiles[uint32(len(c.OpenedFiles))+3] = &FileEntry{Path: dir, FS: fileSys}
+}
+
+// Close implements io.Closer
+func (c *SystemContext) Close() (err error) {
+	// Note: WithStdin, WithStdout and WithStderr are not closed as we didn't open them.
+	// TODO: In #394, close open files
+	return
+}
diff --git a/wasi.go b/wasi.go
index c96d7b27..ef29c1af 100644
--- a/wasi.go
+++ b/wasi.go
@@ -1,7 +1,6 @@
 package wazero
 
 import (
-	"context"
 	"fmt"
 	"io"
 
@@ -69,13 +68,7 @@ func (c *WASIConfig) WithPreopens(preopens map[string]wasi.FS) *WASIConfig {
 
 // WASISnapshotPreview1 are functions importable as the module name wasi.ModuleSnapshotPreview1
 func WASISnapshotPreview1() *Module {
-	return WASISnapshotPreview1WithConfig(&WASIConfig{})
-}
-
-// WASISnapshotPreview1WithConfig are functions importable as the module name wasi.ModuleSnapshotPreview1
-func WASISnapshotPreview1WithConfig(c *WASIConfig) *Module {
-	cfg := newConfig(c) // safe copy of config
-	_, fns := internalwasi.SnapshotPreview1Functions(cfg)
+	_, fns := internalwasi.SnapshotPreview1Functions()
 	m, err := internalwasm.NewHostModule(wasi.ModuleSnapshotPreview1, fns)
 	if err != nil {
 		panic(fmt.Errorf("BUG: %w", err))
@@ -83,19 +76,23 @@ func WASISnapshotPreview1WithConfig(c *WASIConfig) *Module {
 	return &Module{name: wasi.ModuleSnapshotPreview1, module: m}
 }
 
-func newConfig(c *WASIConfig) *internalwasi.Config {
-	cfg := internalwasi.NewConfig()
+func newSystemContext(c *WASIConfig) (*internalwasm.SystemContext, error) {
+	sys, err := internalwasm.NewSystemContext()
+	if err != nil {
+		return nil, err
+	}
+
 	if c.stdin != nil {
-		cfg.Stdin(c.stdin)
+		sys.WithStdin(c.stdin)
 	}
 	if c.stdout != nil {
-		cfg.Stdout(c.stdout)
+		sys.WithStdout(c.stdout)
 	}
 	if c.stderr != nil {
-		cfg.Stderr(c.stderr)
+		sys.WithStderr(c.stderr)
 	}
 	if len(c.args) > 0 {
-		err := cfg.Args(c.args...)
+		err = sys.WithArgs(c.args...)
 		if err != nil {
 			panic(err) // better to panic vs have bother users about unlikely size > uint32
 		}
@@ -105,17 +102,17 @@ func newConfig(c *WASIConfig) *internalwasi.Config {
 		for k, v := range c.environ {
 			environ = append(environ, fmt.Sprintf("%s=%s", k, v))
 		}
-		err := cfg.Environ(environ...)
+		err = sys.WithEnviron(environ...)
 		if err != nil { // this can't be due to lack of '=' as we did that above.
 			panic(err) // better to panic vs have bother users about unlikely size > uint32
 		}
 	}
 	if len(c.preopens) > 0 {
 		for k, v := range c.preopens {
-			cfg.Preopen(k, v)
+			sys.WithPreopen(k, v)
 		}
 	}
-	return cfg
+	return sys, nil
 }
 
 // StartWASICommandFromSource instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or errs if
@@ -172,16 +169,24 @@ func StartWASICommand(r Runtime, module *Module) (wasm.Module, error) {
 //	// Initialize base configuration:
 //	r := wazero.NewRuntime()
 //	config := wazero.NewWASIConfig().WithStdout(buf)
-//	wasi, _ := r.NewHostModule(wazero.WASISnapshotPreview1WithConfig(config))
+//	wasi, _ := r.NewHostModule(wazero.WASISnapshotPreview1())
 //	decoded, _ := r.CompileModule(source)
 //
 //	// Assign configuration only when ready to instantiate.
 //	module, _ := StartWASICommandWithConfig(r, decoded, config.WithArgs("rotate", "angle=90", "dir=cw"))
 //
 // See StartWASICommand
-func StartWASICommandWithConfig(r Runtime, module *Module, config *WASIConfig) (wasm.Module, error) {
-	if err := internalwasi.ValidateWASICommand(module.module, module.name); err != nil {
-		return nil, err
+func StartWASICommandWithConfig(r Runtime, module *Module, config *WASIConfig) (mod wasm.Module, err error) {
+	// Until #394 we have to re-assign the system context manually.
+	var sys *internalwasm.SystemContext
+	if config != nil {
+		if sys, err = newSystemContext(config); err != nil {
+			return
+		}
+	}
+
+	if err = internalwasi.ValidateWASICommand(module.module, module.name); err != nil {
+		return
 	}
 
 	internal, ok := r.(*runtime)
@@ -189,19 +194,17 @@ func StartWASICommandWithConfig(r Runtime, module *Module, config *WASIConfig) (
 		return nil, fmt.Errorf("unsupported Runtime implementation: %s", r)
 	}
 
-	// Override the configuration if needed.
-	ctx := internal.ctx
-	if config != nil {
-		ctx = context.WithValue(ctx, internalwasi.ConfigContextKey{}, newConfig(config)) // safe copy of config
+	if mod, err = internal.store.Instantiate(internal.ctx, module.module, module.name); err != nil {
+		return
 	}
 
-	mod, err := internal.store.Instantiate(ctx, module.module, module.name)
-	if err != nil {
-		return nil, err
+	// Override as necessary
+	if sys != nil {
+		mod.(*internalwasm.ModuleContext).System = sys
 	}
 
 	start := mod.ExportedFunction(internalwasi.FunctionStart)
-	if _, err = start.Call(mod.WithContext(ctx)); err != nil {
+	if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil {
 		return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
 	}
 	return mod, nil


Test patch:
diff --git a/examples/file_system_test.go b/examples/file_system_test.go
index a6be3f16..e1c223c9 100644
--- a/examples/file_system_test.go
+++ b/examples/file_system_test.go
@@ -31,7 +31,7 @@ func Test_Cat(t *testing.T) {
 	stdoutBuf := bytes.NewBuffer(nil)
 	wasiConfig := wazero.NewWASIConfig().WithStdout(stdoutBuf)
 
-	// Next, configure a sand-boxed filesystem to include one file.
+	// Next, configure a sandboxed filesystem to include one file.
 	file := "cat.go" // arbitrary file
 	memFS := wazero.WASIMemFS()
 	err := writeFile(memFS, file, catGo)
@@ -42,13 +42,18 @@ func Test_Cat(t *testing.T) {
 	// Remember, arg[0] is the program name!
 	wasiConfig.WithArgs("cat", file)
 
-	// Now, instantiate WASI with the above configuration.
-	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
+	// Compile the `cat` module.
+	compiled, err := r.CompileModule(catWasm)
+	require.NoError(t, err)
+
+	// Instantiate WASI, which implements system I/O such as console output.
+	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 	defer wasi.Close()
 
-	// Finally, start the `cat` program's main function (in WASI, this is named `_start`).
-	cat, err := wazero.StartWASICommandFromSource(r, catWasm)
+	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to.
+	cat, err := wazero.StartWASICommandWithConfig(r, compiled, wasiConfig)
+
 	require.NoError(t, err)
 	defer cat.Close()
 
diff --git a/examples/host_func_test.go b/examples/host_func_test.go
index 4d80d0f6..60ccedb8 100644
--- a/examples/host_func_test.go
+++ b/examples/host_func_test.go
@@ -59,15 +59,23 @@ func Test_hostFunc(t *testing.T) {
 	_, err := r.NewModuleBuilder("env").ExportFunction("get_random_bytes", getRandomBytes).Instantiate()
 	require.NoError(t, err)
 
-	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
+	// Compile the `hostFunc` module.
+	compiled, err := r.CompileModule(hostFuncWasm)
+	require.NoError(t, err)
+
+	// Configure stdout (console) to write to a buffer.
 	stdout := bytes.NewBuffer(nil)
-	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(wazero.NewWASIConfig().WithStdout(stdout)))
+	config := wazero.NewWASIConfig().WithStdout(stdout)
+
+	// Instantiate WASI, which implements system I/O such as console output.
+	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 	defer wasi.Close()
 
-	module, err := wazero.StartWASICommandFromSource(r, hostFuncWasm)
+	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to.
+	module, err := wazero.StartWASICommandWithConfig(r, compiled, config)
 	require.NoError(t, err)
-	defer module.Close()
+	defer wasi.Close()
 
 	allocateInWasmBufferFn := module.ExportedFunction("allocate_buffer")
 	require.NotNil(t, allocateInWasmBuffer)
diff --git a/examples/stdio_test.go b/examples/stdio_test.go
index afc407d7..09fd5a3b 100644
--- a/examples/stdio_test.go
+++ b/examples/stdio_test.go
@@ -18,20 +18,25 @@ var stdioWasm []byte
 func Test_stdio(t *testing.T) {
 	r := wazero.NewRuntime()
 
+	// Compile the `stdioWasm` module.
+	compiled, err := r.CompileModule(stdioWasm)
+	require.NoError(t, err)
+
+	// Configure standard I/O (ex stdout) to write to buffers instead of no-op.
 	stdinBuf := bytes.NewBuffer([]byte("WASI\n"))
 	stdoutBuf := bytes.NewBuffer(nil)
 	stderrBuf := bytes.NewBuffer(nil)
+	config := wazero.NewWASIConfig().WithStdin(stdinBuf).WithStdout(stdoutBuf).WithStderr(stderrBuf)
 
-	// Configure WASI host functions with the IO buffers
-	wasiConfig := wazero.NewWASIConfig().WithStdin(stdinBuf).WithStdout(stdoutBuf).WithStderr(stderrBuf)
-	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1WithConfig(wasiConfig))
+	// Instantiate WASI, which implements system I/O such as console output.
+	wasi, err := r.InstantiateModule(wazero.WASISnapshotPreview1())
 	require.NoError(t, err)
 	defer wasi.Close()
 
-	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to
-	mod, err := wazero.StartWASICommandFromSource(r, stdioWasm)
+	// StartWASICommand runs the "_start" function which is what TinyGo compiles "main" to.
+	module, err := wazero.StartWASICommandWithConfig(r, compiled, config)
 	require.NoError(t, err)
-	defer mod.Close()
+	defer module.Close()
 
 	require.Equal(t, "Hello, WASI!", strings.TrimSpace(stdoutBuf.String()))
 	require.Equal(t, "Error Message", strings.TrimSpace(stderrBuf.String()))
diff --git a/internal/wasi/strings_test.go b/internal/cstring/cstring_test.go
similarity index 69%
rename from internal/wasi/strings_test.go
rename to internal/cstring/cstring_test.go
index 24dbb029..e53d6b30 100644
--- a/internal/wasi/strings_test.go
+++ b/internal/cstring/cstring_test.go
@@ -1,4 +1,4 @@
-package internalwasi
+package cstring
 
 import (
 	_ "embed"
@@ -8,11 +8,11 @@ import (
 )
 
 func TestNewNullTerminatedStrings(t *testing.T) {
-	emptyWASIStringArray := &nullTerminatedStrings{nullTerminatedValues: [][]byte{}}
+	emptyWASIStringArray := &NullTerminatedStrings{NullTerminatedValues: [][]byte{}}
 	tests := []struct {
 		name     string
 		input    []string
-		expected *nullTerminatedStrings
+		expected *NullTerminatedStrings
 	}{
 		{
 			name:     "nil",
@@ -26,37 +26,37 @@ func TestNewNullTerminatedStrings(t *testing.T) {
 		{
 			name:  "two",
 			input: []string{"a", "bc"},
-			expected: &nullTerminatedStrings{
-				nullTerminatedValues: [][]byte{
+			expected: &NullTerminatedStrings{
+				NullTerminatedValues: [][]byte{
 					{'a', 0},
 					{'b', 'c', 0},
 				},
-				totalBufSize: 5,
+				TotalBufSize: 5,
 			},
 		},
 		{
 			name:  "two and empty string",
 			input: []string{"a", "", "bc"},
-			expected: &nullTerminatedStrings{
-				nullTerminatedValues: [][]byte{
+			expected: &NullTerminatedStrings{
+				NullTerminatedValues: [][]byte{
 					{'a', 0},
 					{0},
 					{'b', 'c', 0},
 				},
-				totalBufSize: 6,
+				TotalBufSize: 6,
 			},
 		},
 		{
 			name: "utf-8",
 			// "😨", "🤣", and "️🏃‍♀️" have 4, 4, and 13 bytes respectively
 			input: []string{"😨🤣🏃\u200d♀️", "foo", "bar"},
-			expected: &nullTerminatedStrings{
-				nullTerminatedValues: [][]byte{
+			expected: &NullTerminatedStrings{
+				NullTerminatedValues: [][]byte{
 					[]byte("😨🤣🏃\u200d♀️\x00"),
 					{'f', 'o', 'o', 0},
 					{'b', 'a', 'r', 0},
 				},
-				totalBufSize: 30,
+				TotalBufSize: 30,
 			},
 		},
 	}
@@ -65,7 +65,7 @@ func TestNewNullTerminatedStrings(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			s, err := newNullTerminatedStrings(100, "", tc.input...)
+			s, err := NewNullTerminatedStrings(100, "", tc.input...)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, s)
 		})
@@ -74,15 +74,15 @@ func TestNewNullTerminatedStrings(t *testing.T) {
 
 func TestNewNullTerminatedStrings_Errors(t *testing.T) {
 	t.Run("invalid utf-8", func(t *testing.T) {
-		_, err := newNullTerminatedStrings(100, "arg", "\xff\xfe\xfd", "foo", "bar")
+		_, err := NewNullTerminatedStrings(100, "arg", "\xff\xfe\xfd", "foo", "bar")
 		require.EqualError(t, err, "arg[0] is not a valid UTF-8 string")
 	})
 	t.Run("arg[0] too large", func(t *testing.T) {
-		_, err := newNullTerminatedStrings(1, "arg", "a", "bc")
+		_, err := NewNullTerminatedStrings(1, "arg", "a", "bc")
 		require.EqualError(t, err, "arg[0] will exceed max buffer size 1")
 	})
 	t.Run("empty arg too large due to null terminator", func(t *testing.T) {
-		_, err := newNullTerminatedStrings(2, "arg", "a", "", "bc")
+		_, err := NewNullTerminatedStrings(2, "arg", "a", "", "bc")
 		require.EqualError(t, err, "arg[1] will exceed max buffer size 2")
 	})
 }
diff --git a/internal/wasi/wasi_test.go b/internal/wasi/wasi_test.go
index c2ada1b7..61b72324 100644
--- a/internal/wasi/wasi_test.go
+++ b/internal/wasi/wasi_test.go
@@ -21,49 +21,12 @@ import (
 
 const moduleName = "test"
 
-func TestConfig_Args(t *testing.T) {
-	t.Run("valid", func(t *testing.T) {
-		config := NewConfig()
-		err := config.Args("a", "bc")
-		require.NoError(t, err)
-
-		require.Equal(t, &nullTerminatedStrings{
-			nullTerminatedValues: [][]byte{
-				{'a', 0},
-				{'b', 'c', 0},
-			},
-			totalBufSize: 5,
-		}, config.args)
-	})
-	t.Run("error constructing args", func(t *testing.T) {
-		err := NewConfig().Args("\xff\xfe\xfd", "foo", "bar")
-		require.EqualError(t, err, "arg[0] is not a valid UTF-8 string")
-	})
-}
-
-func TestWASIAPI_Config(t *testing.T) {
-	t.Run("default when context empty", func(t *testing.T) {
-		config := NewConfig()
-		api := NewAPI(config)
-		require.Same(t, config, api.config(context.Background()))
-	})
-
-	t.Run("overrides default", func(t *testing.T) {
-		config := NewConfig()
-		api := NewAPI(config)
-
-		anotherConfig := NewConfig()
-		require.NotSame(t, anotherConfig, config)
-
-		overridingCtx := context.WithValue(context.Background(), ConfigContextKey{}, anotherConfig)
-		require.Same(t, anotherConfig, api.config(overridingCtx))
-	})
-}
-
 func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Args("a", "bc")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithArgs("a", "bc")
 	require.NoError(t, err)
 
 	argv := uint32(7)    // arbitrary offset
@@ -77,7 +40,7 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 		'?', // stopped after encoding
 	}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.ArgsGet", func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
@@ -106,10 +69,12 @@ func TestSnapshotPreview1_ArgsGet(t *testing.T) {
 
 func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Args("a", "bc")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithArgs("a", "bc")
 	require.NoError(t, err)
-	a, mod, _ := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionArgsGet, ImportArgsGet, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_get. We chose 0 here.
@@ -156,8 +121,10 @@ func TestSnapshotPreview1_ArgsGet_Errors(t *testing.T) {
 
 func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Args("a", "bc")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithArgs("a", "bc")
 	require.NoError(t, err)
 
 	resultArgc := uint32(1)        // arbitrary offset
@@ -170,7 +137,7 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 		'?', // stopped after encoding
 	}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.ArgsSizesGet", func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
@@ -199,11 +166,13 @@ func TestSnapshotPreview1_ArgsSizesGet(t *testing.T) {
 
 func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Args("a", "bc")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithArgs("a", "bc")
 	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionArgsSizesGet, ImportArgsSizesGet, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 	validAddress := uint32(0) // arbitrary valid address as arguments to args_sizes_get. We chose 0 here.
@@ -246,47 +215,12 @@ func TestSnapshotPreview1_ArgsSizesGet_Errors(t *testing.T) {
 	}
 }
 
-func TestConfig_Environ(t *testing.T) {
-	t.Run("valid", func(t *testing.T) {
-		config := NewConfig()
-		err := config.Environ("a=b", "b=cd")
-		require.NoError(t, err)
-
-		require.Equal(t, &nullTerminatedStrings{
-			nullTerminatedValues: [][]byte{
-				{'a', '=', 'b', 0},
-				{'b', '=', 'c', 'd', 0},
-			},
-			totalBufSize: 9,
-		}, config.environ)
-	})
-
-	errorTests := []struct {
-		name         string
-		environ      string
-		errorMessage string
-	}{
-		{name: "error invalid utf-8",
-			environ:      "non_utf8=\xff\xfe\xfd",
-			errorMessage: "environ[0] is not a valid UTF-8 string"},
-		{name: "error not '='-joined pair",
-			environ:      "no_equal_pair",
-			errorMessage: "environ[0] is not joined with '='"},
-	}
-	for _, tt := range errorTests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			err := NewConfig().Environ(tc.environ)
-			require.EqualError(t, err, tc.errorMessage)
-		})
-	}
-}
-
 func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Environ("a=b", "b=cd")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithEnviron("a=b", "b=cd")
 	require.NoError(t, err)
 
 	resultEnviron := uint32(11)   // arbitrary offset
@@ -301,7 +235,7 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 		'?', // stopped after encoding
 	}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.EnvironGet", func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
@@ -330,11 +264,13 @@ func TestSnapshotPreview1_EnvironGet(t *testing.T) {
 
 func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Environ("a=bc", "b=cd")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithEnviron("a=bc", "b=cd")
 	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionEnvironGet, ImportEnvironGet, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 	validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here.
@@ -381,8 +317,10 @@ func TestSnapshotPreview1_EnvironGet_Errors(t *testing.T) {
 
 func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Environ("a=b", "b=cd")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithEnviron("a=b", "b=cd")
 	require.NoError(t, err)
 
 	resultEnvironc := uint32(1)       // arbitrary offset
@@ -395,7 +333,7 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 		'?', // stopped after encoding
 	}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.EnvironSizesGet", func(t *testing.T) {
 		maskMemory(t, mod, len(expectedMemory))
@@ -424,11 +362,13 @@ func TestSnapshotPreview1_EnvironSizesGet(t *testing.T) {
 
 func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
-	err := config.Environ("a=b", "b=cd")
+	sys, err := wasm.NewSystemContext()
 	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, config)
+	err = sys.WithEnviron("a=b", "b=cd")
+	require.NoError(t, err)
+
+	a, mod, _ := instantiateModule(t, ctx, FunctionEnvironSizesGet, ImportEnvironSizesGet, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 	validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here.
@@ -474,9 +414,10 @@ func TestSnapshotPreview1_EnvironSizesGet_Errors(t *testing.T) {
 // TestSnapshotPreview1_ClockResGet only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_ClockResGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionClockResGet, ImportClockResGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionClockResGet, ImportClockResGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.ClockResGet", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.ClockResGet(mod, 0, 0))
@@ -499,9 +440,10 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, sys)
 	a.timeNowUnixNano = func() uint64 { return epochNanos }
 
 	t.Run("SnapshotPreview1.ClockTimeGet", func(t *testing.T) {
@@ -533,9 +475,10 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) {
 	epochNanos := uint64(1640995200000000000) // midnight UTC 2022-01-01
 
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionClockTimeGet, ImportClockTimeGet, moduleName, sys)
 	a.timeNowUnixNano = func() uint64 { return epochNanos }
 
 	memorySize := mod.Memory().Size()
@@ -570,9 +513,10 @@ func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) {
 // TestSnapshotPreview1_FdAdvise only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdAdvise(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdAdvise, ImportFdAdvise, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdAdvise, ImportFdAdvise, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdAdvise", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdAdvise(mod, 0, 0, 0, 0))
@@ -588,9 +532,10 @@ func TestSnapshotPreview1_FdAdvise(t *testing.T) {
 // TestSnapshotPreview1_FdAllocate only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdAllocate(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdAllocate, ImportFdAllocate, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdAllocate, ImportFdAllocate, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdAllocate", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdAllocate(mod, 0, 0, 0))
@@ -609,21 +554,22 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 
 	setupFD := func() (publicwasm.Module, publicwasm.Function, *wasiAPI) {
 		ctx := context.Background()
-		config := NewConfig()
+		sys, err := wasm.NewSystemContext()
+		require.NoError(t, err)
 
 		memFs := &MemFS{}
-		config.opened = map[uint32]fileEntry{
+		sys.OpenedFiles = map[uint32]*wasm.FileEntry{
 			fdToClose: {
-				path:    "/tmp",
-				fileSys: memFs,
+				Path: "/tmp",
+				FS:   memFs,
 			},
 			fdToKeep: {
-				path:    "path to keep",
-				fileSys: memFs,
+				Path: "path to keep",
+				FS:   memFs,
 			},
 		}
 
-		a, mod, fn := instantiateModule(t, ctx, FunctionFdClose, ImportFdClose, moduleName, config)
+		a, mod, fn := instantiateModule(t, ctx, FunctionFdClose, ImportFdClose, moduleName, sys)
 		return mod, fn, a
 	}
 
@@ -632,17 +578,17 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 
 		errno := api.FdClose(mod, fdToClose)
 		require.Equal(t, wasi.ErrnoSuccess, errno)
-		require.NotContains(t, api.cfg.opened, fdToClose) // Fd is closed and removed from the opened FDs.
-		require.Contains(t, api.cfg.opened, fdToKeep)
+		require.NotContains(t, systemContext(mod).OpenedFiles, fdToClose) // Fd is closed and removed from the opened FDs.
+		require.Contains(t, systemContext(mod).OpenedFiles, fdToKeep)
 	})
 	t.Run(FunctionFdClose, func(t *testing.T) {
-		mod, fn, api := setupFD()
+		mod, fn, _ := setupFD()
 
 		ret, err := fn.Call(mod, uint64(fdToClose))
 		require.NoError(t, err)
-		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0])) // cast because results are always uint64
-		require.NotContains(t, api.cfg.opened, fdToClose)       // Fd is closed and removed from the opened FDs.
-		require.Contains(t, api.cfg.opened, fdToKeep)
+		require.Equal(t, wasi.ErrnoSuccess, wasi.Errno(ret[0]))           // cast because results are always uint64
+		require.NotContains(t, systemContext(mod).OpenedFiles, fdToClose) // Fd is closed and removed from the opened FDs.
+		require.Contains(t, systemContext(mod).OpenedFiles, fdToKeep)
 	})
 	t.Run("ErrnoBadF for an invalid FD", func(t *testing.T) {
 		mod, _, api := setupFD()
@@ -655,9 +601,10 @@ func TestSnapshotPreview1_FdClose(t *testing.T) {
 // TestSnapshotPreview1_FdDatasync only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdDatasync(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdDatasync, ImportFdDatasync, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdDatasync, ImportFdDatasync, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdDatasync", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdDatasync(mod, 0))
@@ -675,9 +622,10 @@ func TestSnapshotPreview1_FdDatasync(t *testing.T) {
 // TestSnapshotPreview1_FdFdstatSetFlags only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetFlags, ImportFdFdstatSetFlags, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetFlags, ImportFdFdstatSetFlags, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdFdstatSetFlags", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetFlags(mod, 0, 0))
@@ -693,9 +641,10 @@ func TestSnapshotPreview1_FdFdstatSetFlags(t *testing.T) {
 // TestSnapshotPreview1_FdFdstatSetRights only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetRights, ImportFdFdstatSetRights, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdFdstatSetRights, ImportFdFdstatSetRights, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdFdstatSetRights", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdFdstatSetRights(mod, 0, 0, 0))
@@ -711,9 +660,10 @@ func TestSnapshotPreview1_FdFdstatSetRights(t *testing.T) {
 // TestSnapshotPreview1_FdFilestatGet only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdFilestatGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatGet, ImportFdFilestatGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatGet, ImportFdFilestatGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdFilestatGet", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatGet(mod, 0, 0))
@@ -729,9 +679,10 @@ func TestSnapshotPreview1_FdFilestatGet(t *testing.T) {
 // TestSnapshotPreview1_FdFilestatSetSize only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetSize, ImportFdFilestatSetSize, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetSize, ImportFdFilestatSetSize, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdFilestatSetSize", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetSize(mod, 0, 0))
@@ -747,9 +698,10 @@ func TestSnapshotPreview1_FdFilestatSetSize(t *testing.T) {
 // TestSnapshotPreview1_FdFilestatSetTimes only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetTimes, ImportFdFilestatSetTimes, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdFilestatSetTimes, ImportFdFilestatSetTimes, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdFilestatSetTimes", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdFilestatSetTimes(mod, 0, 0, 0, 0))
@@ -765,9 +717,10 @@ func TestSnapshotPreview1_FdFilestatSetTimes(t *testing.T) {
 // TestSnapshotPreview1_FdPread only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdPread(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdPread, ImportFdPread, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdPread, ImportFdPread, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdPread", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdPread(mod, 0, 0, 0, 0, 0))
@@ -784,12 +737,12 @@ func TestSnapshotPreview1_FdPrestatGet(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[fd] = fileEntry{
-		path: "/tmp",
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "/tmp"}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys)
 
 	resultPrestat := uint32(1) // arbitrary offset
 	expectedMemory := []byte{
@@ -830,12 +783,12 @@ func TestSnapshotPreview1_FdPrestatGet_Errors(t *testing.T) {
 	validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_get. We chose 0 here.
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened = map[uint32]fileEntry{
-		fd: {path: "/tmp"},
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, config)
+	sys.OpenedFiles = map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}
+
+	a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatGet, ImportFdPrestatGet, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 
@@ -874,13 +827,12 @@ func TestSnapshotPreview1_FdPrestatDirName(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[fd] = fileEntry{
-		path:    "/tmp",
-		fileSys: &MemFS{},
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "/tmp", FS: &MemFS{}}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, sys)
 
 	path := uint32(1)    // arbitrary offset
 	pathLen := uint32(3) // shorter than len("/tmp") to test the path is written for the length of pathLen
@@ -918,13 +870,13 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) {
 	fd := uint32(3) // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened = map[uint32]fileEntry{
-		fd: {path: "/tmp"},
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles = map[uint32]*wasm.FileEntry{fd: {Path: "/tmp"}}
 	pathLen := uint32(len("/tmp"))
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionFdPrestatDirName, ImportFdPrestatDirName, moduleName, sys)
 
 	memorySize := mod.Memory().Size()
 	validAddress := uint32(0) // Arbitrary valid address as arguments to fd_prestat_dir_name. We chose 0 here.
@@ -980,9 +932,10 @@ func TestSnapshotPreview1_FdPrestatDirName_Errors(t *testing.T) {
 // TestSnapshotPreview1_FdPwrite only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdPwrite(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdPwrite, ImportFdPwrite, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdPwrite, ImportFdPwrite, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdPwrite", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdPwrite(mod, 0, 0, 0, 0, 0))
@@ -997,7 +950,8 @@ func TestSnapshotPreview1_FdPwrite(t *testing.T) {
 
 func TestSnapshotPreview1_FdRead(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
 	fd := uint32(3)   // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 	iovs := uint32(1) // arbitrary offset
@@ -1021,7 +975,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 		'?',
 	)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, sys)
 
 	// TestSnapshotPreview1_FdRead uses a matrix because setting up test files is complicated and has to be clean each time.
 	type fdReadFn func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno
@@ -1046,11 +1000,7 @@ func TestSnapshotPreview1_FdRead(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			// Create a fresh file to read the contents from
 			file, memFS := createFile(t, "test_path", []byte("wazero"))
-			config.opened[fd] = fileEntry{
-				path:    "test_path",
-				fileSys: memFS,
-				file:    file,
-			}
+			sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
 			maskMemory(t, mod, len(expectedMemory))
 
 			ok := mod.Memory().Write(0, initialMemory)
@@ -1071,14 +1021,12 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) {
 	file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[validFD] = fileEntry{
-		path:    "test_path",
-		fileSys: memFS,
-		file:    file,
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionFdRead, ImportFdRead, moduleName, sys)
 
 	tests := []struct {
 		name                            string
@@ -1163,9 +1111,10 @@ func TestSnapshotPreview1_FdRead_Errors(t *testing.T) {
 // TestSnapshotPreview1_FdReaddir only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdReaddir(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdReaddir, ImportFdReaddir, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdReaddir, ImportFdReaddir, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdReaddir", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdReaddir(mod, 0, 0, 0, 0, 0))
@@ -1181,9 +1130,10 @@ func TestSnapshotPreview1_FdReaddir(t *testing.T) {
 // TestSnapshotPreview1_FdRenumber only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdRenumber(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdRenumber, ImportFdRenumber, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdRenumber, ImportFdRenumber, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdRenumber", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdRenumber(mod, 0, 0))
@@ -1202,14 +1152,12 @@ func TestSnapshotPreview1_FdSeek(t *testing.T) {
 	file, memFS := createFile(t, "test_path", []byte("wazero")) // arbitrary non-empty contents
 
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[fd] = fileEntry{
-		path:    "test_path",
-		fileSys: memFS,
-		file:    file,
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, sys)
 
 	// 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
@@ -1298,14 +1246,12 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
 	validFD := uint32(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
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[validFD] = fileEntry{
-		path:    "test_path",
-		fileSys: memFS,
-		file:    file,
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, config)
+	sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
+
+	a, mod, _ := instantiateModule(t, ctx, FunctionFdSeek, ImportFdSeek, moduleName, sys)
 	memorySize := mod.Memory().Size()
 
 	tests := []struct {
@@ -1347,9 +1293,10 @@ func TestSnapshotPreview1_FdSeek_Errors(t *testing.T) {
 // TestSnapshotPreview1_FdSync only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdSync(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdSync, ImportFdSync, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdSync, ImportFdSync, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdSync", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdSync(mod, 0))
@@ -1365,9 +1312,10 @@ func TestSnapshotPreview1_FdSync(t *testing.T) {
 // TestSnapshotPreview1_FdTell only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_FdTell(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdTell, ImportFdTell, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdTell, ImportFdTell, moduleName, sys)
 
 	t.Run("SnapshotPreview1.FdTell", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.FdTell(mod, 0, 0))
@@ -1382,7 +1330,8 @@ func TestSnapshotPreview1_FdTell(t *testing.T) {
 
 func TestSnapshotPreview1_FdWrite(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
 	fd := uint32(3)   // arbitrary fd after 0, 1, and 2, that are stdin/out/err
 	iovs := uint32(1) // arbitrary offset
@@ -1406,7 +1355,7 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 		'?',
 	)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, sys)
 
 	// TestSnapshotPreview1_FdWrite uses a matrix because setting up test files is complicated and has to be clean each time.
 	type fdWriteFn func(ctx publicwasm.Module, fd, iovs, iovsCount, resultSize uint32) wasi.Errno
@@ -1431,11 +1380,8 @@ func TestSnapshotPreview1_FdWrite(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			// Create a fresh file to write the contents to
 			file, memFS := createFile(t, "test_path", []byte{})
-			config.opened[fd] = fileEntry{
-				path:    "test_path",
-				fileSys: memFS,
-				file:    file,
-			}
+			sys.OpenedFiles[fd] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
+
 			maskMemory(t, mod, len(expectedMemory))
 			ok := mod.Memory().Write(0, initialMemory)
 			require.True(t, ok)
@@ -1455,14 +1401,12 @@ func TestSnapshotPreview1_FdWrite_Errors(t *testing.T) {
 	validFD := uint32(3)                                // arbitrary valid fd after 0, 1, and 2, that are stdin/out/err
 	file, memFS := createFile(t, "test_path", []byte{}) // file with empty contents
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened[uint32(validFD)] = fileEntry{
-		path:    "test_path",
-		fileSys: memFS,
-		file:    file,
-	}
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles[validFD] = &wasm.FileEntry{Path: "test_path", FS: memFS, File: file}
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionFdWrite, ImportFdWrite, moduleName, sys)
 
 	// Setup valid test memory
 	iovs, iovsCount := uint32(0), uint32(1)
@@ -1532,18 +1476,19 @@ 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)
 
-	memFile := f.(*memFile)
-	memFile.buf = append([]byte{}, contents...)
+	mf := f.(*memFile)
+	mf.buf = append([]byte{}, contents...)
 
-	return memFile, memFS
+	return mf, memFS
 }
 
 // TestSnapshotPreview1_PathCreateDirectory only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathCreateDirectory, ImportPathCreateDirectory, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathCreateDirectory, ImportPathCreateDirectory, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathCreateDirectory", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathCreateDirectory(mod, 0, 0, 0))
@@ -1559,9 +1504,10 @@ func TestSnapshotPreview1_PathCreateDirectory(t *testing.T) {
 // TestSnapshotPreview1_PathFilestatGet only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathFilestatGet(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatGet, ImportPathFilestatGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatGet, ImportPathFilestatGet, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathFilestatGet", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathFilestatGet(mod, 0, 0, 0, 0, 0))
@@ -1577,9 +1523,10 @@ func TestSnapshotPreview1_PathFilestatGet(t *testing.T) {
 // TestSnapshotPreview1_PathFilestatSetTimes only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatSetTimes, ImportPathFilestatSetTimes, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathFilestatSetTimes, ImportPathFilestatSetTimes, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathFilestatSetTimes", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathFilestatSetTimes(mod, 0, 0, 0, 0, 0, 0, 0))
@@ -1595,9 +1542,10 @@ func TestSnapshotPreview1_PathFilestatSetTimes(t *testing.T) {
 // TestSnapshotPreview1_PathLink only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathLink(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathLink, ImportPathLink, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathLink, ImportPathLink, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathLink", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathLink(mod, 0, 0, 0, 0, 0, 0, 0))
@@ -1641,9 +1589,10 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys)
 	// randSource is used to determine the new fd. Fix it to the expectedFD for testing.
 	a.randSource = func(b []byte) error {
 		binary.LittleEndian.PutUint32(b, expectedFD)
@@ -1691,11 +1640,8 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
 				tc := tt
 				t.Run(tc.name, func(t *testing.T) {
 					// Set up a fresh opened FD table
-					config.opened = map[uint32]fileEntry{
-						workdirFD: {
-							path:    ".",
-							fileSys: memFS,
-						},
+					sys.OpenedFiles = map[uint32]*wasm.FileEntry{
+						workdirFD: {Path: ".", FS: memFS},
 					}
 
 					maskMemory(t, mod, len(expectedMemory))
@@ -1708,7 +1654,8 @@ func TestSnapshotPreview1_PathOpen(t *testing.T) {
 					actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
 					require.True(t, ok)
 					require.Equal(t, expectedMemory, actual)
-					require.Equal(t, tc.expectedPath, config.opened[expectedFD].path) // verify the file was actually opened
+					// verify the file was actually opened
+					require.Equal(t, tc.expectedPath, sys.OpenedFiles[expectedFD].Path)
 				})
 			}
 		})
@@ -1724,15 +1671,14 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 		},
 	}
 	ctx := context.Background()
-	config := NewConfig()
-	config.opened = map[uint32]fileEntry{
-		validFD: {
-			path:    ".",
-			fileSys: memFS,
-		},
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	sys.OpenedFiles = map[uint32]*wasm.FileEntry{
+		validFD: {Path: ".", FS: memFS},
 	}
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionPathOpen, ImportPathOpen, moduleName, sys)
 
 	validPath := uint32(0)    // arbitrary offset
 	validPathLen := uint32(6) // the length of "wazero"
@@ -1793,9 +1739,10 @@ func TestSnapshotPreview1_PathOpen_Errors(t *testing.T) {
 // TestSnapshotPreview1_PathReadlink only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathReadlink(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathReadlink, ImportPathReadlink, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathReadlink, ImportPathReadlink, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathLink", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathReadlink(mod, 0, 0, 0, 0, 0, 0))
@@ -1811,9 +1758,10 @@ func TestSnapshotPreview1_PathReadlink(t *testing.T) {
 // TestSnapshotPreview1_PathRemoveDirectory only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathRemoveDirectory, ImportPathRemoveDirectory, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathRemoveDirectory, ImportPathRemoveDirectory, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathRemoveDirectory", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathRemoveDirectory(mod, 0, 0, 0))
@@ -1829,9 +1777,10 @@ func TestSnapshotPreview1_PathRemoveDirectory(t *testing.T) {
 // TestSnapshotPreview1_PathRename only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathRename(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathRename, ImportPathRename, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathRename, ImportPathRename, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathRename", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathRename(mod, 0, 0, 0, 0, 0, 0))
@@ -1847,9 +1796,10 @@ func TestSnapshotPreview1_PathRename(t *testing.T) {
 // TestSnapshotPreview1_PathSymlink only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathSymlink(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathSymlink, ImportPathSymlink, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathSymlink, ImportPathSymlink, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathSymlink", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathSymlink(mod, 0, 0, 0, 0, 0))
@@ -1865,9 +1815,10 @@ func TestSnapshotPreview1_PathSymlink(t *testing.T) {
 // TestSnapshotPreview1_PathUnlinkFile only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPathUnlinkFile, ImportPathUnlinkFile, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPathUnlinkFile, ImportPathUnlinkFile, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PathUnlinkFile", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PathUnlinkFile(mod, 0, 0, 0))
@@ -1883,9 +1834,10 @@ func TestSnapshotPreview1_PathUnlinkFile(t *testing.T) {
 // TestSnapshotPreview1_PollOneoff only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_PollOneoff(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionPollOneoff, ImportPollOneoff, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionPollOneoff, ImportPollOneoff, moduleName, sys)
 
 	t.Run("SnapshotPreview1.PollOneoff", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.PollOneoff(mod, 0, 0, 0, 0))
@@ -1900,7 +1852,8 @@ func TestSnapshotPreview1_PollOneoff(t *testing.T) {
 
 func TestSnapshotPreview1_ProcExit(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
 	tests := []struct {
 		name     string
@@ -1919,7 +1872,7 @@ func TestSnapshotPreview1_ProcExit(t *testing.T) {
 
 	// Note: Unlike most tests, this uses fn, not the 'a' result parameter. This is because currently, this function
 	// body panics, and we expect Call to unwrap the panic.
-	_, mod, fn := instantiateModule(t, ctx, FunctionProcExit, ImportProcExit, moduleName, config)
+	_, mod, fn := instantiateModule(t, ctx, FunctionProcExit, ImportProcExit, moduleName, sys)
 
 	for _, tt := range tests {
 		tc := tt
@@ -1937,9 +1890,10 @@ func TestSnapshotPreview1_ProcExit(t *testing.T) {
 // TestSnapshotPreview1_ProcRaise only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_ProcRaise(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionProcRaise, ImportProcRaise, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionProcRaise, ImportProcRaise, moduleName, sys)
 
 	t.Run("SnapshotPreview1.ProcRaise", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.ProcRaise(mod, 0))
@@ -1955,9 +1909,10 @@ func TestSnapshotPreview1_ProcRaise(t *testing.T) {
 // TestSnapshotPreview1_SchedYield only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_SchedYield(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionSchedYield, ImportSchedYield, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionSchedYield, ImportSchedYield, moduleName, sys)
 
 	t.Run("SnapshotPreview1.SchedYield", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.SchedYield(mod))
@@ -1981,9 +1936,10 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
 	offset := uint32(1) // offset,
 	seed := int64(42)   // and seed value
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys)
 	a.randSource = func(p []byte) error {
 		s := rand.NewSource(seed)
 		rng := rand.New(s)
@@ -2019,11 +1975,12 @@ func TestSnapshotPreview1_RandomGet(t *testing.T) {
 
 func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
 	validAddress := uint32(0) // arbitrary valid address
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys)
 	memorySize := mod.Memory().Size()
 
 	tests := []struct {
@@ -2056,9 +2013,10 @@ func TestSnapshotPreview1_RandomGet_Errors(t *testing.T) {
 
 func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, config)
+	a, mod, _ := instantiateModule(t, ctx, FunctionRandomGet, ImportRandomGet, moduleName, sys)
 	a.randSource = func(p []byte) error {
 		return errors.New("random source error")
 	}
@@ -2070,9 +2028,10 @@ func TestSnapshotPreview1_RandomGet_SourceError(t *testing.T) {
 // TestSnapshotPreview1_SockRecv only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_SockRecv(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionSockRecv, ImportSockRecv, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionSockRecv, ImportSockRecv, moduleName, sys)
 
 	t.Run("SnapshotPreview1.SockRecv", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.SockRecv(mod, 0, 0, 0, 0, 0, 0))
@@ -2088,9 +2047,10 @@ func TestSnapshotPreview1_SockRecv(t *testing.T) {
 // TestSnapshotPreview1_SockSend only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_SockSend(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionSockSend, ImportSockSend, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionSockSend, ImportSockSend, moduleName, sys)
 
 	t.Run("SnapshotPreview1.SockSend", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.SockSend(mod, 0, 0, 0, 0, 0))
@@ -2106,9 +2066,10 @@ func TestSnapshotPreview1_SockSend(t *testing.T) {
 // TestSnapshotPreview1_SockShutdown only tests it is stubbed for GrainLang per #271
 func TestSnapshotPreview1_SockShutdown(t *testing.T) {
 	ctx := context.Background()
-	config := NewConfig()
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
 
-	a, mod, fn := instantiateModule(t, ctx, FunctionSockShutdown, ImportSockShutdown, moduleName, config)
+	a, mod, fn := instantiateModule(t, ctx, FunctionSockShutdown, ImportSockShutdown, moduleName, sys)
 
 	t.Run("SnapshotPreview1.SockShutdown", func(t *testing.T) {
 		require.Equal(t, wasi.ErrnoNosys, a.SockShutdown(mod, 0, 0))
@@ -2123,20 +2084,20 @@ func TestSnapshotPreview1_SockShutdown(t *testing.T) {
 
 const testMemoryPageSize = 1
 
-func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, config *Config) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) {
+func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImport, moduleName string, sys *wasm.SystemContext) (*wasiAPI, *wasm.ModuleContext, publicwasm.Function) {
 	enabledFeatures := wasm.Features20191205
 	store := wasm.NewStore(interpreter.NewEngine(), enabledFeatures)
 
 	// The package `wazero` has a simpler interface for adding host modules, but we can't use that as it would create an
 	// import cycle. Instead, we export internalwasm.NewHostModule and use it here.
-	a, fns := SnapshotPreview1Functions(config)
+	a, fns := SnapshotPreview1Functions()
 	m, err := wasm.NewHostModule(wasi.ModuleSnapshotPreview1, fns)
 	require.NoError(t, err)
 
 	// Double-check what we created passes same validity as module-defined modules.
 	require.NoError(t, m.Validate(enabledFeatures))
 
-	_, err = store.Instantiate(ctx, m, m.NameSection.ModuleName)
+	_, err = store.Instantiate(ctx, m, m.NameSection.ModuleName) // TODO: close
 	require.NoError(t, err)
 
 	m, err = text.DecodeModule([]byte(fmt.Sprintf(`(module
@@ -2149,6 +2110,7 @@ func instantiateModule(t *testing.T, ctx context.Context, wasiFunction, wasiImpo
 
 	mod, err := store.Instantiate(ctx, m, moduleName)
 	require.NoError(t, err)
+	mod.System = sys // TODO: temporary until #394
 
 	fn := mod.ExportedFunction(wasiFunction)
 	require.NotNil(t, fn)
diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go
index 0870d68f..9fcdda01 100644
--- a/internal/wasm/interpreter/interpreter_test.go
+++ b/internal/wasm/interpreter/interpreter_test.go
@@ -208,6 +208,7 @@ func TestEngine_Call_HostFn(t *testing.T) {
 	e := NewEngine()
 	module := &wasm.ModuleInstance{Memory: memory}
 	modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module)
+
 	f := &wasm.FunctionInstance{
 		GoFunc: &hostFn,
 		Kind:   wasm.FunctionKindGoModule,
diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go
index 5069b83d..bf350805 100644
--- a/internal/wasm/jit/engine_test.go
+++ b/internal/wasm/jit/engine_test.go
@@ -267,6 +267,7 @@ func TestEngine_Call_HostFn(t *testing.T) {
 	e := NewEngine()
 	module := &wasm.ModuleInstance{Memory: memory}
 	modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module)
+
 	f := &wasm.FunctionInstance{
 		GoFunc: &hostFn,
 		Kind:   wasm.FunctionKindGoModule,
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index 47ab9a8c..84b5cbec 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -5,12 +5,14 @@ import (
 	"encoding/binary"
 	"fmt"
 	"math"
+	"os"
 	"strconv"
 	"sync"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 
+	"github.com/tetratelabs/wazero/internal/cstring"
 	"github.com/tetratelabs/wazero/wasm"
 )
 
@@ -87,7 +89,7 @@ func TestModuleInstance_Memory(t *testing.T) {
 	}
 }
 
-func TestPublicModule_String(t *testing.T) {
+func TestModuleContext_String(t *testing.T) {
 	s := newStore()
 
 	// Ensure paths that can create the host module can see the name.
@@ -97,6 +99,31 @@ func TestPublicModule_String(t *testing.T) {
 	require.Equal(t, "Module[module]", s.Module(m.module.Name).String())
 }
 
+func TestStore_Instantiate(t *testing.T) {
+	s := newStore()
+	m, err := NewHostModule("", map[string]interface{}{"fn": func(wasm.Module) {}})
+	require.NoError(t, err)
+
+	type key string
+	ctx := context.WithValue(context.Background(), key("a"), "b") // arbitrary non-default context
+	mod, err := s.Instantiate(ctx, m, "")
+	require.NoError(t, err)
+	defer mod.Close()
+
+	t.Run("ModuleContext defaults", func(t *testing.T) {
+		require.Equal(t, ctx, mod.ctx)
+		require.Equal(t, s.modules[""], mod.module)
+		require.Equal(t, s.modules[""].Memory, mod.memory)
+		require.Equal(t, s, mod.store)
+		require.Equal(t, cstring.EmptyNullTerminatedStrings, mod.System.Args)
+		require.Equal(t, cstring.EmptyNullTerminatedStrings, mod.System.Environ)
+		require.Equal(t, os.Stdin, mod.System.Stdin)
+		require.Equal(t, os.Stdout, mod.System.Stdout)
+		require.Equal(t, os.Stderr, mod.System.Stderr)
+		require.Empty(t, mod.System.OpenedFiles)
+	})
+}
+
 func TestStore_CloseModule(t *testing.T) {
 	const importedModuleName = "imported"
 	const importingModuleName = "test"
diff --git a/internal/wasm/sys_test.go b/internal/wasm/sys_test.go
new file mode 100644
index 00000000..8771c155
--- /dev/null
+++ b/internal/wasm/sys_test.go
@@ -0,0 +1,89 @@
+package internalwasm
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/wazero/internal/cstring"
+)
+
+func TestSystemContext_Defaults(t *testing.T) {
+	sys, err := NewSystemContext()
+	require.Nil(t, err)
+
+	require.Equal(t, cstring.EmptyNullTerminatedStrings, sys.Args)
+	require.Equal(t, cstring.EmptyNullTerminatedStrings, sys.Environ)
+	require.Equal(t, os.Stdin, sys.Stdin)
+	require.Equal(t, os.Stdout, sys.Stdout)
+	require.Equal(t, os.Stderr, sys.Stderr)
+	require.Empty(t, sys.OpenedFiles)
+}
+
+func TestSystemContext_WithArgs(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		sys, err := NewSystemContext()
+		require.Nil(t, err)
+
+		err = sys.WithArgs("a", "bc")
+		require.NoError(t, err)
+
+		require.Equal(t, &cstring.NullTerminatedStrings{
+			NullTerminatedValues: [][]byte{
+				{'a', 0},
+				{'b', 'c', 0},
+			},
+			TotalBufSize: 5,
+		}, sys.Args)
+	})
+	t.Run("error constructing args", func(t *testing.T) {
+		sys, err := NewSystemContext()
+		require.Nil(t, err)
+
+		err = sys.WithArgs("\xff\xfe\xfd", "foo", "bar")
+		require.EqualError(t, err, "arg[0] is not a valid UTF-8 string")
+	})
+}
+
+func TestSystemContext_WithEnviron(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		sys, err := NewSystemContext()
+		require.Nil(t, err)
+
+		err = sys.WithEnviron("a=b", "b=cd")
+		require.NoError(t, err)
+
+		require.Equal(t, &cstring.NullTerminatedStrings{
+			NullTerminatedValues: [][]byte{
+				{'a', '=', 'b', 0},
+				{'b', '=', 'c', 'd', 0},
+			},
+			TotalBufSize: 9,
+		}, sys.Environ)
+	})
+
+	errorTests := []struct {
+		name         string
+		environ      string
+		errorMessage string
+	}{
+		{name: "error invalid utf-8",
+			environ:      "non_utf8=\xff\xfe\xfd",
+			errorMessage: "environ[0] is not a valid UTF-8 string"},
+		{name: "error not '='-joined pair",
+			environ:      "no_equal_pair",
+			errorMessage: "environ[0] is not joined with '='"},
+	}
+	for _, tt := range errorTests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			sys, err := NewSystemContext()
+			require.Nil(t, err)
+
+			err = sys.WithEnviron(tc.environ)
+			require.EqualError(t, err, tc.errorMessage)
+		})
+	}
+}
diff --git a/tests/bench/wasi_test.go b/tests/bench/wasi_test.go
index 1500b27f..565ffb94 100644
--- a/tests/bench/wasi_test.go
+++ b/tests/bench/wasi_test.go
@@ -11,52 +11,68 @@ import (
 	"github.com/tetratelabs/wazero/wasi"
 )
 
-var ctx = wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{
-	Memory: &wasm.MemoryInstance{
-		Min: 1,
-		Buffer: []byte{
-			0,                // environBuf is after this
-			'a', '=', 'b', 0, // null terminated "a=b",
-			'b', '=', 'c', 'd', 0, // null terminated "b=cd"
-			0,          // environ is after this
-			1, 0, 0, 0, // little endian-encoded offset of "a=b"
-			5, 0, 0, 0, // little endian-encoded offset of "b=cd"
-			0,
-		},
+var testMem = &wasm.MemoryInstance{
+	Min: 1,
+	Buffer: []byte{
+		0,                // environBuf is after this
+		'a', '=', 'b', 0, // null terminated "a=b",
+		'b', '=', 'c', 'd', 0, // null terminated "b=cd"
+		0,          // environ is after this
+		1, 0, 0, 0, // little endian-encoded offset of "a=b"
+		5, 0, 0, 0, // little endian-encoded offset of "b=cd"
+		0,
 	},
-})
+}
 
 func Test_EnvironGet(t *testing.T) {
-	config := internalwasi.NewConfig()
-	err := config.Environ("a=b", "b=cd")
+	sys, err := wasm.NewSystemContext()
+	require.NoError(t, err)
+
+	err = sys.WithEnviron("a=b", "b=cd")
 	require.NoError(t, err)
 
-	testCtx := newCtx(make([]byte, 20))
-	environGet := internalwasi.NewAPI(config).EnvironGet
+	testCtx := newCtx(make([]byte, 20), sys)
+	environGet := internalwasi.NewAPI().EnvironGet
 
 	require.Equal(t, wasi.ErrnoSuccess, environGet(testCtx, 11, 1))
-	require.Equal(t, testCtx.Memory(), ctx.Memory())
+	require.Equal(t, testCtx.Memory(), testMem)
 }
 
 func Benchmark_EnvironGet(b *testing.B) {
-	config := internalwasi.NewConfig()
-	err := config.Environ("a=b", "b=cd")
+	sys, err := wasm.NewSystemContext()
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	err = sys.WithEnviron("a=b", "b=cd")
 	if err != nil {
 		b.Fatal(err)
 	}
 
-	environGet := internalwasi.NewAPI(config).EnvironGet
+	testCtx := newCtx([]byte{
+		0,                // environBuf is after this
+		'a', '=', 'b', 0, // null terminated "a=b",
+		'b', '=', 'c', 'd', 0, // null terminated "b=cd"
+		0,          // environ is after this
+		1, 0, 0, 0, // little endian-encoded offset of "a=b"
+		5, 0, 0, 0, // little endian-encoded offset of "b=cd"
+		0,
+	}, sys)
+
+	environGet := internalwasi.NewAPI().EnvironGet
 	b.Run("EnvironGet", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
-			if environGet(ctx, 0, 4) != wasi.ErrnoSuccess {
+			if environGet(testCtx, 0, 4) != wasi.ErrnoSuccess {
 				b.Fatal()
 			}
 		}
 	})
 }
 
-func newCtx(buf []byte) *wasm.ModuleContext {
-	return wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{
+func newCtx(buf []byte, sys *wasm.SystemContext) *wasm.ModuleContext {
+	ret := wasm.NewModuleContext(context.Background(), nil, &wasm.ModuleInstance{
 		Memory: &wasm.MemoryInstance{Min: 1, Buffer: buf},
 	})
+	ret.System = sys
+	return ret
 }
diff --git a/wasi_test.go b/wasi_test.go
index f34ffc11..f52154ce 100644
--- a/wasi_test.go
+++ b/wasi_test.go
@@ -54,7 +54,7 @@ func TestStartWASICommandWithConfig(t *testing.T) {
 
 	// Configure WASI with baseline config
 	config := NewWASIConfig().WithStdout(stdout)
-	wasi, err := r.InstantiateModule(WASISnapshotPreview1WithConfig(config))
+	wasi, err := r.InstantiateModule(WASISnapshotPreview1())
 	require.NoError(t, err)
 	defer wasi.Close()