Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't panic on overallocation and remove mux #8

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading