Skip to content

Commit

Permalink
Don't panic on overallocation and remove mux (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
anuraaga authored Jan 23, 2025
1 parent 3dff157 commit cd30c44
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 65 deletions.
2 changes: 1 addition & 1 deletion allocator/nonmoving_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package allocator

import "github.com/tetratelabs/wazero/experimental"

var pageSize = 0 // used only for test
var pageSize = uint64(0) // used only for test

func alloc(cap, max uint64) experimental.LinearMemory {
return sliceAlloc(cap, max)
Expand Down
3 changes: 0 additions & 3 deletions allocator/nonmoving_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,5 @@ type sliceBuffer struct {
func (b *sliceBuffer) Free() {}

func (b *sliceBuffer) Reallocate(size uint64) []byte {
if int(size) > cap(b.buf) {
panic(errInvalidReallocation)
}
return b.buf[:size]
}
6 changes: 2 additions & 4 deletions allocator/nonmoving_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestNonMoving(t *testing.T) {
tests := []struct {
name string
mem experimental.LinearMemory
cap int
cap uint64
}{
{
name: "native",
Expand All @@ -36,7 +36,7 @@ func TestNonMoving(t *testing.T) {

buf := mem.Reallocate(5)
require.Len(t, buf, 5)
require.Equal(t, tc.cap, cap(buf))
require.EqualValues(t, tc.cap, cap(buf))
base := &buf[0]

buf = mem.Reallocate(5)
Expand All @@ -50,8 +50,6 @@ func TestNonMoving(t *testing.T) {
buf = mem.Reallocate(20)
require.Len(t, buf, 20)
require.Equal(t, base, &buf[0])

require.PanicsWithError(t, errInvalidReallocation.Error(), func() { mem.Reallocate(21) })
})
}
}
40 changes: 12 additions & 28 deletions allocator/nonmoving_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,55 @@ package allocator
import (
"fmt"
"math"
"sync"

"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/unix"
)

var pageSize = unix.Getpagesize()
var pageSize = uint64(unix.Getpagesize())

func alloc(_, max uint64) experimental.LinearMemory {
// Round up to the page size because recommitting must be page-aligned.
// In practice, the WebAssembly page size should be a multiple of the system
// page size on most if not all platforms and rounding will never happen.
rnd := uint64(pageSize - 1)
reserved := (max + rnd) &^ rnd
rnd := pageSize - 1
res := (max + rnd) &^ rnd

if reserved > math.MaxInt {
if res > math.MaxInt {
// This ensures int(max) overflows to a negative value,
// and unix.Mmap returns EINVAL.
reserved = math.MaxUint64
res = math.MaxUint64
}

// Reserve max bytes of address space, to ensure we won't need to move it.
// A protected, private, anonymous mapping should not commit memory.
b, err := unix.Mmap(-1, 0, int(reserved), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
b, err := unix.Mmap(-1, 0, int(res), unix.PROT_NONE, unix.MAP_PRIVATE|unix.MAP_ANON)
if err != nil {
panic(fmt.Errorf("allocator_unix: failed to reserve memory: %w", err))
}
return &mmappedMemory{buf: b[:0], max: max}
return &mmappedMemory{buf: b[:0]}
}

// The slice covers the entire mmapped memory:
// - len(buf) is the already committed memory,
// - cap(buf) is the reserved address space, which is max rounded up to a page.
type mmappedMemory struct {
buf []byte
max uint64

// Any reasonable Wasm implementation will take a lock before calling Grow, but this
// is invisible to Go's race detector so it can still detect raciness when we updated
// buf. We go ahead and take a lock when mutating since the performance effect should
// be negligible in practice and it will help the race detector confirm the safety.
mu sync.Mutex
}

func (m *mmappedMemory) Reallocate(size uint64) []byte {
if size > m.max {
panic(errInvalidReallocation)
}

m.mu.Lock()
defer m.mu.Unlock()

com := uint64(len(m.buf))
if com < size {
res := uint64(cap(m.buf))

if com < size && size <= res {
// Round up to the page size.
rnd := uint64(unix.Getpagesize() - 1)
rnd := pageSize - 1
newCap := (size + rnd) &^ rnd

// Commit additional memory up to new bytes.
err := unix.Mprotect(m.buf[com:newCap], unix.PROT_READ|unix.PROT_WRITE)
if err != nil {
panic(fmt.Errorf("allocator_unix: failed to commit memory: %w", err))
return nil
}

// Update committed memory.
Expand All @@ -78,9 +65,6 @@ func (m *mmappedMemory) Reallocate(size uint64) []byte {
}

func (m *mmappedMemory) Free() {
m.mu.Lock()
defer m.mu.Unlock()

err := unix.Munmap(m.buf[:cap(m.buf)])
if err != nil {
panic(fmt.Errorf("allocator_unix: failed to release memory: %w", err))
Expand Down
42 changes: 13 additions & 29 deletions allocator/nonmoving_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,36 @@ package allocator
import (
"fmt"
"math"
"sync"
"unsafe"

"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/windows"
)

var pageSize = windows.Getpagesize()
var pageSize = uint64(windows.Getpagesize())

func alloc(_, max uint64) experimental.LinearMemory {
// Round up to the page size because recommitting must be page-aligned.
// In practice, the WebAssembly page size should be a multiple of the system
// page size on most if not all platforms and rounding will never happen.
rnd := uint64(pageSize) - 1
reserved := (max + rnd) &^ rnd
rnd := pageSize - 1
res := (max + rnd) &^ rnd

if reserved > math.MaxInt {
if res > math.MaxInt {
// This ensures uintptr(max) overflows to a large value,
// and windows.VirtualAlloc returns an error.
reserved = math.MaxUint64
res = math.MaxUint64
}

// Reserve max bytes of address space, to ensure we won't need to move it.
// This does not commit memory.
r, err := windows.VirtualAlloc(0, uintptr(reserved), windows.MEM_RESERVE, windows.PAGE_READWRITE)
r, err := windows.VirtualAlloc(0, uintptr(res), windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
panic(fmt.Errorf("allocator_windows: failed to reserve memory: %w", err))
}

buf := unsafe.Slice((*byte)(unsafe.Pointer(r)), int(reserved))
return &virtualMemory{buf: buf[:0], addr: r, max: max}
buf := unsafe.Slice((*byte)(unsafe.Pointer(r)), int(res))
return &virtualMemory{buf: buf[:0], addr: r}
}

// The slice covers the entire mmapped memory:
Expand All @@ -44,33 +43,21 @@ func alloc(_, max uint64) experimental.LinearMemory {
type virtualMemory struct {
buf []byte
addr uintptr
max uint64

// Any reasonable Wasm implementation will take a lock before calling Grow, but this
// is invisible to Go's race detector so it can still detect raciness when we updated
// buf. We go ahead and take a lock when mutating since the performance effect should
// be negligible in practice and it will help the race detector confirm the safety.
mu sync.Mutex
}

func (m *virtualMemory) Reallocate(size uint64) []byte {
if size > m.max {
panic(errInvalidReallocation)
}

m.mu.Lock()
defer m.mu.Unlock()

com := uint64(len(m.buf))
if com < size {
res := uint64(cap(m.buf))

if com < size && size <= res {
// Round up to the page size.
rnd := uint64(pageSize) - 1
rnd := pageSize - 1
newCap := (size + rnd) &^ rnd

// Commit additional memory up to new bytes.
_, err := windows.VirtualAlloc(m.addr, uintptr(newCap), windows.MEM_COMMIT, windows.PAGE_READWRITE)
if err != nil {
panic(fmt.Errorf("allocator_windows: failed to commit memory: %w", err))
return nil
}

// Update committed memory.
Expand All @@ -82,9 +69,6 @@ func (m *virtualMemory) Reallocate(size uint64) []byte {
}

func (m *virtualMemory) Free() {
m.mu.Lock()
defer m.mu.Unlock()

err := windows.VirtualFree(m.addr, 0, windows.MEM_RELEASE)
if err != nil {
panic(fmt.Errorf("allocator_windows: failed to release memory: %w", err))
Expand Down

0 comments on commit cd30c44

Please sign in to comment.