Repository: tetratelabs/wazero (go)

Issue:
Is it possible to grow api.Memory programmatically?
There are scenarios where we may grow shared memory programmatically:

```go
module, _ := v.runtime.InstantiateModuleFromCode(ctx, policy)
module.Memory().Grow(1) // error: unresolved reference 'Grow'
```

However, the `Grow` method is not defined on the `api.Memory` interface. It's only defined on the internal `MemoryInstance` struct:

```go
func (m *MemoryInstance) Grow(newPages uint32) (result uint32) {
	// [...]
}
```

Is it intentionally designed like that, or there is another API to grow shared memory?

Pull request:
Exposes api.Memory Grow
This exports an API which allows host-defined functions access to
increase the memory size.

Fixes #483

Patch:
diff --git a/api/wasm.go b/api/wasm.go
index 2ed07497..e82a7f51 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -221,14 +221,21 @@ type MutableGlobal interface {
 // Note: This includes all value types available in WebAssembly 1.0 (20191205) and all are encoded little-endian.
 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#storage%E2%91%A0
 type Memory interface {
+
 	// Size returns the size in bytes available. Ex. If the underlying memory has 1 page: 65536
 	//
-	// Note: this will not grow during a host function call, even if the underlying memory can.  Ex. If the underlying
-	// memory has min 0 and max 2 pages, this returns zero.
-	//
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
 	Size(context.Context) uint32
 
+	// Grow increases memory by the delta in pages (65536 bytes per page). The return val is the previous memory size in
+	// pages, or false if the delta was ignored as it exceeds max memory.
+	//
+	// Note: This is the same as the "memory.grow" instruction defined in the WebAssembly Core Specification, except
+	// returns false instead of -1 on failure
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
+	// See MemorySizer
+	Grow(ctx context.Context, deltaPages uint32) (previousPages uint32, ok bool)
+
 	// IndexByte returns the index of the first instance of c in the underlying buffer at the offset or returns false if
 	// not found or out of range.
 	IndexByte(ctx context.Context, offset uint32, c byte) (uint32, bool)
diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go
index 7295aadf..50daffcc 100644
--- a/internal/wasm/interpreter/interpreter.go
+++ b/internal/wasm/interpreter/interpreter.go
@@ -882,8 +882,11 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, callCtx *wasm.CallCont
 		case wazeroir.OperationKindMemoryGrow:
 			{
 				n := ce.popValue()
-				res := memoryInst.Grow(ctx, uint32(n))
-				ce.pushValue(uint64(res))
+				if res, ok := memoryInst.Grow(ctx, uint32(n)); !ok {
+					ce.pushValue(uint64(0xffffffff)) // = -1 in signed 32-bit integer.
+				} else {
+					ce.pushValue(uint64(res))
+				}
 				frame.pc++
 			}
 		case wazeroir.OperationKindConstI32, wazeroir.OperationKindConstI64,
diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go
index 9da7e491..648a8a64 100644
--- a/internal/wasm/jit/engine.go
+++ b/internal/wasm/jit/engine.go
@@ -774,8 +774,11 @@ func (ce *callEngine) builtinFunctionGrowCallFrameStack() {
 func (ce *callEngine) builtinFunctionMemoryGrow(ctx context.Context, mem *wasm.MemoryInstance) {
 	newPages := ce.popValue()
 
-	res := mem.Grow(ctx, uint32(newPages))
-	ce.pushValue(uint64(res))
+	if res, ok := mem.Grow(ctx, uint32(newPages)); !ok {
+		ce.pushValue(uint64(0xffffffff)) // = -1 in signed 32-bit integer.
+	} else {
+		ce.pushValue(uint64(res))
+	}
 
 	// Update the moduleContext fields as they become stale after the update ^^.
 	bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&mem.Buffer))
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index c695ce49..3e99e0f2 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"math"
 	"reflect"
+	"sync"
 	"unsafe"
 
 	"github.com/tetratelabs/wazero/api"
@@ -44,6 +45,8 @@ var _ api.Memory = &MemoryInstance{}
 type MemoryInstance struct {
 	Buffer        []byte
 	Min, Cap, Max uint32
+	// mux is used to prevent overlapping calls to Grow.
+	mux sync.RWMutex
 }
 
 // NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory.
@@ -211,31 +214,31 @@ func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) {
 	return uint64(pages) << MemoryPageSizeInBits
 }
 
-// Grow extends the memory buffer by "newPages" * memoryPageSize.
-// The logic here is described in https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem.
-//
-// Returns -1 if the operation resulted in exceeding the maximum memory pages.
-// Otherwise, returns the prior memory size after growing the memory buffer.
-func (m *MemoryInstance) Grow(_ context.Context, delta uint32) (result uint32) {
+// Grow implements the same method as documented on api.Memory.
+func (m *MemoryInstance) Grow(_ context.Context, delta uint32) (result uint32, ok bool) {
 	// Note: If you use the context.Context param, don't forget to coerce nil to context.Background()!
 
+	// We take write-lock here as the following might result in a new slice
+	m.mux.Lock()
+	defer m.mux.Unlock()
+
 	currentPages := memoryBytesNumToPages(uint64(len(m.Buffer)))
 	if delta == 0 {
-		return currentPages
+		return currentPages, true
 	}
 
 	// If exceeds the max of memory size, we push -1 according to the spec.
 	newPages := currentPages + delta
 	if newPages > m.Max {
-		return 0xffffffff // = -1 in signed 32-bit integer.
+		return 0, false
 	} else if newPages > m.Cap { // grow the memory.
 		m.Buffer = append(m.Buffer, make([]byte, MemoryPagesToBytesNum(delta))...)
 		m.Cap = newPages
-		return currentPages
+		return currentPages, true
 	} else { // We already have the capacity we need.
 		sp := (*reflect.SliceHeader)(unsafe.Pointer(&m.Buffer))
 		sp.Len = int(MemoryPagesToBytesNum(newPages))
-		return currentPages
+		return currentPages, true
 	}
 }
 
@@ -274,7 +277,7 @@ func memoryBytesNumToPages(bytesNum uint64) (pages uint32) {
 
 // size returns the size in bytes of the buffer.
 func (m *MemoryInstance) size() uint32 {
-	return uint32(len(m.Buffer))
+	return uint32(len(m.Buffer)) // We don't lock here because size can't become smaller.
 }
 
 // hasSize returns true if Len is sufficient for sizeInBytes at the given offset.


Test patch:
diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go
index 43d4f7b0..ba65d5ec 100644
--- a/internal/wasm/memory_test.go
+++ b/internal/wasm/memory_test.go
@@ -66,22 +66,33 @@ func TestMemoryInstance_Grow_Size(t *testing.T) {
 			} else {
 				m = &MemoryInstance{Max: max, Buffer: make([]byte, 0)}
 			}
-			require.Equal(t, uint32(0), m.Grow(ctx, 5))
+
+			res, ok := m.Grow(ctx, 5)
+			require.True(t, ok)
+			require.Equal(t, uint32(0), res)
 			require.Equal(t, uint32(5), m.PageSize(ctx))
 
 			// Zero page grow is well-defined, should return the current page correctly.
-			require.Equal(t, uint32(5), m.Grow(ctx, 0))
+			res, ok = m.Grow(ctx, 0)
+			require.True(t, ok)
+			require.Equal(t, uint32(5), res)
 			require.Equal(t, uint32(5), m.PageSize(ctx))
-			require.Equal(t, uint32(5), m.Grow(ctx, 4))
+
+			res, ok = m.Grow(ctx, 4)
+			require.True(t, ok)
+			require.Equal(t, uint32(5), res)
 			require.Equal(t, uint32(9), m.PageSize(ctx))
 
 			// At this point, the page size equal 9,
 			// so trying to grow two pages should result in failure.
-			require.Equal(t, int32(-1), int32(m.Grow(ctx, 2)))
+			_, ok = m.Grow(ctx, 2)
+			require.False(t, ok)
 			require.Equal(t, uint32(9), m.PageSize(ctx))
 
 			// But growing one page is still permitted.
-			require.Equal(t, uint32(9), m.Grow(ctx, 1))
+			res, ok = m.Grow(ctx, 1)
+			require.True(t, ok)
+			require.Equal(t, uint32(9), res)
 
 			// Ensure that the current page size equals the max.
 			require.Equal(t, max, m.PageSize(ctx))