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. 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))