Repository: tetratelabs/wazero
Pull request #216 (2022-02-09 02:41:18)

wasi: Implement environ_get and environ_sizes_get API
This PR implements `environ_get` and `environ_sizes_get`. 

nullpo-head: Hi @mathetake @codefromthecrypt, I've updated this PR based on the latest code base. PTAL. Thanks!

Patch:
diff --git a/wasi/strings.go b/wasi/strings.go
index bf4a01ce..e0ce8f9c 100644
--- a/wasi/strings.go
+++ b/wasi/strings.go
@@ -18,21 +18,21 @@ type nullTerminatedStrings struct {
 }
 
 // newNullTerminatedStrings creates a nullTerminatedStrings from the given string slice. It returns an error
-// if the length or the total buffer size of the result WASIStringArray exceeds the maxBufSize
-func newNullTerminatedStrings(maxBufSize uint32, args ...string) (*nullTerminatedStrings, error) {
-	if len(args) == 0 {
+// 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
 	}
 	var strings [][]byte // don't pre-allocate as this function is size bound
 	totalBufSize := uint32(0)
-	for i, arg := range args {
+	for i, arg := range vals {
 		if !utf8.ValidString(arg) {
-			return nil, fmt.Errorf("arg[%d] is not a valid UTF-8 string", i)
+			return nil, fmt.Errorf("%s[%d] is not a valid UTF-8 string", valName, i)
 		}
 		argLen := uint64(len(arg)) + 1 // + 1 for "\x00"; uint64 in case this one arg is huge
 		nextSize := uint64(totalBufSize) + argLen
 		if nextSize > uint64(maxBufSize) {
-			return nil, fmt.Errorf("arg[%d] will exceed max buffer size %d", i, maxBufSize)
+			return nil, fmt.Errorf("%s[%d] will exceed max buffer size %d", valName, i, maxBufSize)
 		}
 		totalBufSize = uint32(nextSize)
 		strings = append(strings, append([]byte(arg), 0))
diff --git a/wasi/wasi.go b/wasi/wasi.go
index 949c91ca..8f8cfca6 100644
--- a/wasi/wasi.go
+++ b/wasi/wasi.go
@@ -3,12 +3,14 @@ package wasi
 import (
 	crand "crypto/rand"
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"math"
 	mrand "math/rand"
 	"os"
 	"reflect"
+	"strings"
 	"time"
 
 	"github.com/tetratelabs/wazero/wasm"
@@ -82,9 +84,60 @@ type API interface {
 	// See https://en.wikipedia.org/wiki/Null-terminated_string
 	ArgsSizesGet(ctx wasm.HostFunctionCallContext, resultArgc, resultArgvBufSize uint32) Errno
 
-	// TODO: EnvironGet(ctx api.hostFunctionCallContext, environ, environBuf uint32) Errno
+	// EnvironGet is the WASI function that reads environment variables. (Environ)
+	//
+	// There are two parameters. Both are offsets in wasm.HostFunctionCallContext Memory. If either are invalid due to
+	// memory constraints, this returns ErrnoFault.
+	//
+	// * environ - is the offset to begin writing environment variables offsets in uint32 little-endian encoding.
+	//   * EnvironSizesGet result environc * 4 bytes are written to this offset
+	// * environBuf - is the offset to write the null terminated environment variables in the form of "key=val", the same format as os.Environ(), to wasm.MemoryInstance.
+	//   * EnvironSizesGet result environBufSize bytes are written to this offset
+	//
+	// For example, if EnvironSizesGet wrote environc=2 and environBufSize=9 for environment variables: "a=b", "b=cd"
+	//   and EnvironGet parameters environ=11 and environBuf=1, we expect `ctx.Memory.Buffer` to contain:
+	//
+	//                           environBufSize                 uint32le    uint32le
+	//              +------------------------------------+     +--------+  +--------+
+	//              |                                    |     |        |  |        |
+	//   []byte{?, 'a', '=', 'b', 0, 'b', '=', 'c', 'd', 0, ?, 1, 0, 0, 0, 5, 0, 0, 0, ?}
+	// environBuf --^                                          ^           ^
+	//                              environ offset for "a=b" --+           |
+	//                                         environ offset for "b=cd" --+
+	//
+	// Note: ImportEnvironGet shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// See EnvironSizesGet
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
+	// See https://en.wikipedia.org/wiki/Null-terminated_string
+	EnvironGet(ctx wasm.HostFunctionCallContext, environ, environBuf uint32) Errno
 
-	// TODO: EnvironSizesGet(ctx api.hostFunctionCallContext, resulEnvironc, resultEnvironBufSize uint32) Errno
+	// EnvironSizesGet is a WASI function that reads environment variable (Environ) sizes.
+	//
+	// There are two result parameters: these are offsets in the wasi.HostFunctionCallContext Memory to write
+	// corresponding sizes in uint32 little-endian encoding. If either are invalid due to memory constraints, this
+	// returns ErrnoFault.
+	//
+	// * resultEnvironc - is the offset to write the environment variable count to wasm.MemoryInstance Buffer
+	// * resultEnvironBufSize - is the offset to write the null-terminated environment variable length to wasm.MemoryInstance Buffer
+	//
+	// For example, if Environ is []string{"a=b","b=cd"} and
+	//   EnvironSizesGet parameters are resultEnvironc=1 and resultEncironBufSize=6, we expect `ctx.Memory.Buffer` to contain:
+	//
+	//                   uint32le       uint32le
+	//                  +--------+     +--------+
+	//                  |        |     |        |
+	//        []byte{?, 2, 0, 0, 0, ?, 9, 0, 0, 0, ?}
+	// resultEnvironc --^              ^
+	//    2 variables --+              |
+	//          resultEnvironBufSize --|
+	//    len([]byte{'a','=','b',0,    |
+	//           'b','=','c','d',0}) --+
+	//
+	// Note: EnvironSizesGet documentation shows this signature in the WebAssembly 1.0 (MVP) Text Format.
+	// See EnvironGet
+	// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
+	// See https://en.wikipedia.org/wiki/Null-terminated_string
+	EnvironSizesGet(ctx wasm.HostFunctionCallContext, resultEnvironc, resultEnvironBufSize uint32) Errno
 
 	// TODO: ClockResGet(ctx api.hostFunctionCallContext, id, resultResolution uint32) Errno
 
@@ -174,8 +227,11 @@ const (
 )
 
 type wasiAPI struct {
-	args  *nullTerminatedStrings
-	stdin io.Reader
+	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
@@ -193,8 +249,8 @@ func (a *wasiAPI) register(store *wasm.Store) (err error) {
 	}{
 		{FunctionArgsGet, a.ArgsGet},
 		{FunctionArgsSizesGet, a.ArgsSizesGet},
-		{FunctionEnvironGet, environ_get},
-		{FunctionEnvironSizesGet, environ_sizes_get},
+		{FunctionEnvironGet, a.EnvironGet},
+		{FunctionEnvironSizesGet, a.EnvironSizesGet},
 		// TODO: FunctionClockResGet
 		{FunctionClockTimeGet, a.ClockTimeGet},
 		// TODO: FunctionFdAdvise
@@ -278,9 +334,34 @@ func (a *wasiAPI) ArgsSizesGet(ctx wasm.HostFunctionCallContext, resultArgc, res
 	return ErrnoSuccess
 }
 
-// TODO: func (a *wasiAPI) EnvironGet
+// EnvironGet implements API.EnvironGet
+func (w *wasiAPI) EnvironGet(ctx wasm.HostFunctionCallContext, environ uint32, environBuf uint32) (err Errno) {
+	// w.environ holds the environment variables in the form of "key=val\x00", so just copies it to the linear memory.
+	for _, env := range w.environ.nullTerminatedValues {
+		if !ctx.Memory().WriteUint32Le(environ, environBuf) {
+			return ErrnoFault
+		}
+		environ += 4 // size of uint32
+		if !ctx.Memory().Write(environBuf, env) {
+			return ErrnoFault
+		}
+		environBuf += uint32(len(env))
+	}
+
+	return ErrnoSuccess
+}
+
+// EnvironSizesGet implements API.EnvironSizesGet
+func (w *wasiAPI) EnvironSizesGet(ctx wasm.HostFunctionCallContext, resultEnvironc uint32, resultEnvironBufSize uint32) (err Errno) {
+	if !ctx.Memory().WriteUint32Le(resultEnvironc, uint32(len(w.environ.nullTerminatedValues))) {
+		return ErrnoFault
+	}
+	if !ctx.Memory().WriteUint32Le(resultEnvironBufSize, w.environ.totalBufSize) {
+		return ErrnoFault
+	}
 
-// TODO: func (a *wasiAPI) EnvironSizesGet
+	return ErrnoSuccess
+}
 
 // TODO: func (a *wasiAPI) FunctionClockResGet
 
@@ -324,7 +405,7 @@ func Stderr(writer io.Writer) Option {
 // Note: The only reason to set this is to control what's written by API.ArgsSizesGet and API.ArgsGet
 // Note: While similar in structure to os.Args, this controls what's visible in Wasm (ex the WASI function "_start").
 func Args(args ...string) (Option, error) {
-	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, args...) // TODO: this is crazy high even if spec allows it
+	wasiStrings, err := newNullTerminatedStrings(math.MaxUint32, "arg", args...) // TODO: this is crazy high even if spec allows it
 	if err != nil {
 		return nil, err
 	}
@@ -333,6 +414,29 @@ func Args(args ...string) (Option, error) {
 	}, nil
 }
 
+// Environ returns an option to set environment variables to the API.
+// 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 API.EnvironSizesGet and API.EnvironGet
+// Note: While similar in structure to os.Environ, this controls what's visible in Wasm (ex the WASI function "_start").
+func Environ(environ ...string) (Option, error) {
+	for i, env := range environ {
+		if !strings.Contains(env, "=") {
+			return nil, 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 nil, err
+	}
+	return func(w *wasiAPI) {
+		w.environ = wasiStrings
+	}, nil
+}
+
 func Preopen(dir string, fileSys FS) Option {
 	return func(a *wasiAPI) {
 		a.opened[uint32(len(a.opened))+3] = fileEntry{
@@ -357,11 +461,12 @@ func registerAPI(store *wasm.Store, opts ...Option) (API, error) {
 
 func newAPI(opts ...Option) *wasiAPI {
 	ret := &wasiAPI{
-		args:   &nullTerminatedStrings{},
-		stdin:  os.Stdin,
-		stdout: os.Stdout,
-		stderr: os.Stderr,
-		opened: map[uint32]fileEntry{},
+		args:    &nullTerminatedStrings{},
+		environ: &nullTerminatedStrings{},
+		stdin:   os.Stdin,
+		stdout:  os.Stdout,
+		stderr:  os.Stderr,
+		opened:  map[uint32]fileEntry{},
 		timeNowUnixNano: func() uint64 {
 			return uint64(time.Now().UnixNano())
 		},
@@ -580,11 +685,3 @@ func (a *wasiAPI) RandomGet(ctx wasm.HostFunctionCallContext, buf uint32, bufLen
 func proc_exit(wasm.HostFunctionCallContext, uint32) {
 	// TODO: implement
 }
-
-func environ_sizes_get(wasm.HostFunctionCallContext, uint32, uint32) (err Errno) {
-	return ErrnoNosys // TODO: implement
-}
-
-func environ_get(wasm.HostFunctionCallContext, uint32, uint32) (err Errno) {
-	return ErrnoNosys // TODO: implement
-}
diff --git a/wasm/jit/jit_amd64.go b/wasm/jit/jit_amd64.go
index 51c15f56..c18a51d0 100644
--- a/wasm/jit/jit_amd64.go
+++ b/wasm/jit/jit_amd64.go
@@ -4616,12 +4616,12 @@ func (c *amd64Compiler) callFunction(addr wasm.FunctionAddress, addrReg int16, f
 		return err
 	}
 
-	// After the function call, we have to initialize the stack base pointer and memory reserved registers.
-	c.initializeReservedStackBasePointer()
-	c.initializeReservedMemoryPointer()
-
 	// For call_indirect, we need to push the value back to the register.
 	if !isNilRegister(addrReg) {
+		// Since this is right after callGoFunction, we have to initialize the stack base pointer
+		// to properly load the value on memory stack.
+		c.initializeReservedStackBasePointer()
+
 		savedOffsetLocation := c.locationStack.pop()
 		savedOffsetLocation.setRegister(addrReg)
 		c.moveStackToRegister(savedOffsetLocation)
@@ -5212,14 +5212,13 @@ func (c *amd64Compiler) compilePreamble() (err error) {
 	// the caller.
 	c.pushFunctionParams()
 
-	// Initialize the reserved stack base pointer register.
-	c.initializeReservedStackBasePointer()
-
 	// Check if it's necessary to grow the value stack by using max stack pointer.
 	if err = c.maybeGrowValueStack(); err != nil {
 		return err
 	}
 
+	c.initializeReservedStackBasePointer()
+
 	// Once the stack base pointer is initialized and the size of stack is ok,
 	// initialize the module context next.
 	if err := c.initializeModuleContext(); err != nil {
@@ -5333,8 +5332,6 @@ func (c *amd64Compiler) maybeGrowValueStack() error {
 	if err := c.callGoFunction(jitCallStatusCodeCallBuiltInFunction, builtinFunctionAddressGrowValueStack); err != nil {
 		return err
 	}
-	// After grow the stack, we have to inialize the stack base pointer again.
-	c.initializeReservedStackBasePointer()
 
 	c.addSetJmpOrigins(jmpIfNoNeedToGrowStack)
 	return nil


Test patch:
diff --git a/wasi/strings_test.go b/wasi/strings_test.go
index d9eb0d98..b93ba39a 100644
--- a/wasi/strings_test.go
+++ b/wasi/strings_test.go
@@ -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, "\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, "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, "a", "", "bc")
+		_, err := newNullTerminatedStrings(2, "arg", "a", "", "bc")
 		require.EqualError(t, err, "arg[1] will exceed max buffer size 2")
 	})
 }
diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go
index 5e4831cf..359d3dc1 100644
--- a/wasi/wasi_test.go
+++ b/wasi/wasi_test.go
@@ -53,10 +53,10 @@ func TestAPI_ArgsGet(t *testing.T) {
 	t.Run("API.ArgsGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		// provide a host context we call directly
+		// Provide a host context we call directly.
 		hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])
 
-		// invoke ArgsGet directly and check the memory side-effects!
+		// Invoke ArgsGet directly and check the memory side-effects!
 		errno := wasiAPI.ArgsGet(hContext, argv, argvBuf)
 		require.Equal(t, ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
@@ -136,10 +136,10 @@ func TestAPI_ArgsSizesGet(t *testing.T) {
 	t.Run("API.ArgsSizesGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		// provide a host context we call directly
+		// Provide a host context we call directly.
 		hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])
 
-		// invoke ArgsSizesGet directly and check the memory side effects!
+		// Invoke ArgsSizesGet directly and check the memory side effects!
 		errno := wasiAPI.ArgsSizesGet(hContext, resultArgc, resultArgvBufSize)
 		require.Equal(t, ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
@@ -200,8 +200,210 @@ func TestAPI_ArgsSizesGet_Errors(t *testing.T) {
 	}
 }
 
-// TODO TestAPI_EnvironGet TestAPI_EnvironGet_Errors
-// TODO TestAPI_EnvironSizesGet TestAPI_EnvironSizesGet_Errors
+func TestNewAPI_Environ(t *testing.T) {
+	t.Run("valid", func(t *testing.T) {
+		o, err := Environ("a=b", "b=cd")
+		require.NoError(t, err)
+		a := newAPI(o)
+		require.Equal(t, &nullTerminatedStrings{
+			nullTerminatedValues: [][]byte{
+				{'a', '=', 'b', 0},
+				{'b', '=', 'c', 'd', 0},
+			},
+			totalBufSize: 9,
+		}, a.environ)
+	})
+
+	errorTests := []struct {
+		name        string
+		environ     string
+		errorMesage string
+	}{
+		{name: "error invalid utf-8",
+			environ:     "non_utf8=\xff\xfe\xfd",
+			errorMesage: "environ[0] is not a valid UTF-8 string"},
+		{name: "error not '='-joined pair",
+			environ:     "no_equal_pair",
+			errorMesage: "environ[0] is not joined with '='"},
+	}
+	for _, tt := range errorTests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := Environ(tc.environ)
+			require.EqualError(t, err, tc.errorMesage)
+		})
+	}
+}
+
+func TestAPI_EnvironGet(t *testing.T) {
+	envOpt, err := Environ("a=b", "b=cd")
+	require.NoError(t, err)
+	resultEnviron := uint32(11)   // arbitrary offset
+	resultEnvironBuf := uint32(1) // arbitrary offset
+	maskLength := 20              // number of bytes to write '?' to tell what we've written
+	expectedMemory := []byte{
+		'?',              // environBuf is after this
+		'a', '=', 'b', 0, // null terminated "a=b",
+		'b', '=', 'c', 'd', 0, // null terminated "b=cd"
+		'?',        // 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"
+		'?', // stopped after encoding
+	}
+	store, wasiAPI := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
+
+	t.Run("API.EnvironGet", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		// Provide a host context we call directly.
+		hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])
+
+		// Invoke EnvironGet directly and check the memory side-effects.
+		errno := wasiAPI.EnvironGet(hContext, resultEnviron, resultEnvironBuf)
+		require.Equal(t, ErrnoSuccess, errno)
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+	t.Run(FunctionEnvironGet, func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironGet, uint64(resultEnviron), uint64(resultEnvironBuf))
+		require.NoError(t, err)
+		require.Equal(t, ErrnoSuccess, Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+}
+
+func TestAPI_EnvironGet_Errors(t *testing.T) {
+	envOpt, err := Environ("a=bc", "b=cd")
+	require.NoError(t, err)
+	store, api := instantiateWasmStore(t, FunctionEnvironGet, ImportEnvironGet, "test", envOpt)
+
+	memorySize := uint32(len(store.Memories[0].Buffer))
+	validAddress := uint32(0) // arbitrary valid address as arguments to environ_get. We chose 0 here.
+
+	tests := []struct {
+		name       string
+		environ    uint32
+		environBuf uint32
+	}{
+		{
+			name:       "out-of-memory environPtr",
+			environ:    memorySize,
+			environBuf: validAddress,
+		},
+		{
+			name:       "out-of-memory environBufPtr",
+			environ:    validAddress,
+			environBuf: memorySize,
+		},
+		{
+			name: "environPtr exceeds the maximum valid address by 1",
+			// 4*len(environ.nullTerminatedValues) is the expected buffer size for environPtr, 4 is the size of uint32
+			environ:    memorySize - 4*uint32(len(api.(*wasiAPI).environ.nullTerminatedValues)) + 1,
+			environBuf: validAddress,
+		},
+		{
+			name:       "environBufPtr exceeds the maximum valid address by 1",
+			environ:    validAddress,
+			environBuf: memorySize - api.(*wasiAPI).environ.totalBufSize + 1,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironGet, uint64(tc.environ), uint64(tc.environBuf))
+			require.NoError(t, err)
+			require.Equal(t, uint64(ErrnoFault), ret[0]) // ret[0] is returned errno
+		})
+	}
+}
+
+func TestAPI_EnvironSizesGet(t *testing.T) {
+	envOpt, err := Environ("a=b", "b=cd")
+	require.NoError(t, err)
+	resultEnvironc := uint32(1)       // arbitrary offset
+	resultEnvironBufSize := uint32(6) // arbitrary offset
+	maskLength := 11                  // number of bytes to write '?' to tell what we've written
+	expectedMemory := []byte{
+		'?',                // resultEnvironc is after this
+		0x2, 0x0, 0x0, 0x0, // little endian-encoded environment variable count
+		'?',                // resultEnvironBufSize is after this
+		0x9, 0x0, 0x0, 0x0, // little endian-encoded size of null terminated strings
+		'?', // stopped after encoding
+	}
+	store, wasiAPI := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
+
+	t.Run("API.EnvironSizesGet", func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		// Provide a host context we call directly.
+		hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])
+
+		// Invoke EnvironSizesGet directly and check the memory side effects.
+		errno := wasiAPI.EnvironSizesGet(hContext, resultEnvironc, resultEnvironBufSize)
+		require.Equal(t, ErrnoSuccess, errno)
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+	t.Run(FunctionEnvironSizesGet, func(t *testing.T) {
+		maskMemory(store, maskLength)
+
+		ret, _, err := store.CallFunction(context.Background(), "test", FunctionEnvironSizesGet, uint64(resultEnvironc), uint64(resultEnvironBufSize))
+		require.NoError(t, err)
+		require.Equal(t, ErrnoSuccess, Errno(ret[0])) // cast because results are always uint64
+		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])
+	})
+}
+
+func TestAPI_EnvironSizesGet_Errors(t *testing.T) {
+	ctx := context.Background()
+	envOpt, err := Environ("a=b", "b=cd")
+	require.NoError(t, err)
+	store, _ := instantiateWasmStore(t, FunctionEnvironSizesGet, ImportEnvironSizesGet, "test", envOpt)
+
+	memorySize := uint32(len(store.Memories[0].Buffer))
+	validAddress := uint32(0) // arbitrary valid address as arguments to environ_sizes_get. We chose 0 here.
+
+	tests := []struct {
+		name           string
+		environc       uint32
+		environBufSize uint32
+	}{
+		{
+			name:           "out-of-memory environCountPtr",
+			environc:       memorySize,
+			environBufSize: validAddress,
+		},
+		{
+			name:           "out-of-memory environBufSizePtr",
+			environc:       validAddress,
+			environBufSize: memorySize,
+		},
+		{
+			name:           "environCountPtr exceeds the maximum valid address by 1",
+			environc:       memorySize - 4 + 1, // 4 is the size of uint32, the type of the count of environ
+			environBufSize: validAddress,
+		},
+		{
+			name:           "environBufSizePtr exceeds the maximum valid size by 1",
+			environc:       validAddress,
+			environBufSize: memorySize - 4 + 1, // 4 is the size of uint32, the type of the buffer size
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+
+		t.Run(tc.name, func(t *testing.T) {
+			ret, _, err := store.CallFunction(ctx, "test", FunctionEnvironSizesGet, uint64(tc.environc), uint64(tc.environBufSize))
+			require.NoError(t, err)
+			require.Equal(t, uint64(ErrnoFault), ret[0]) // ret[0] is returned errno
+		})
+	}
+}
+
 // TODO TestAPI_ClockResGet TestAPI_ClockResGet_Errors
 
 func TestAPI_ClockTimeGet(t *testing.T) {
@@ -220,10 +422,10 @@ func TestAPI_ClockTimeGet(t *testing.T) {
 	t.Run("API.ClockTimeGet", func(t *testing.T) {
 		maskMemory(store, maskLength)
 
-		// provide a host context we call directly
+		// Provide a host context we call directly.
 		hContext := wasm.NewHostFunctionCallContext(context.Background(), store.Memories[0])
 
-		// invoke ClockTimeGet directly and check the memory side effects!
+		// Invoke ClockTimeGet directly and check the memory side effects!
 		errno := api.ClockTimeGet(hContext, 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp)
 		require.Equal(t, ErrnoSuccess, errno)
 		require.Equal(t, expectedMemory, store.Memories[0].Buffer[0:maskLength])