From c6bd70121619a01e1ee53c62d7ed32446b7580e9 Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Sat, 31 Jan 2026 12:53:29 -0700 Subject: [PATCH] mem: add windows memory allocation primitives, add basic tests --- mem/mem.go | 33 +++++++++++++++++ mem/mem_test.go | 52 +++++++++++++++++++++++++++ mem/mem_unix.go | 33 ++++++++++------- mem/mem_windows.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 mem/mem_windows.go diff --git a/mem/mem.go b/mem/mem.go index b6c9e5a..a7e51ba 100644 --- a/mem/mem.go +++ b/mem/mem.go @@ -119,3 +119,36 @@ func ExtendSlice[T any](slice []T, amount uintptr) []T { return slice[: uintptr(len(slice))+amount : cap(slice)] } + +// Access describes memory access permissions. +type Access int + +const ( + AccessNone Access = 1 << iota + AccessRead + AccessWrite + AccessExecute +) + +// Reserve returns a slice of bytes pointing to uncommitted virtual memory. +// The length and capacity of the slice will be total_address_space bytes. +// +// The underlying memory of the slice must be comitted to phyiscal memory before being accessed (see: Commit). +// +// Use Release to return the virtual address space back to the operating system. +func Reserve(total_address_space uintptr) ([]byte, error) { return reserve(total_address_space) } + +// Release returns reserved virtual address space back to the operating system. +// +// Note: Any committed memory within its address space will be freed as well. +func Release(reserved []byte) error { return release(reserved) } + +// Commit maps virtual memory to physical memory. +func Commit(reserved []byte, access Access) error { return commit(reserved, access) } + +// Decommit unmaps committed memory, leaving the underlying addresss space intact. +// +// Decommitted memory can be re-committed at a later time using Commit. +// +// Note: Accessing the memory after calling Decommit is unsafe and may cause a panic. +func Decommit(committed []byte) (err error) { return decommit(committed) } diff --git a/mem/mem_test.go b/mem/mem_test.go index a645f81..1d22501 100644 --- a/mem/mem_test.go +++ b/mem/mem_test.go @@ -2,8 +2,10 @@ package mem_test import ( "testing" + "unsafe" "git.brut.systems/judah/xx/mem" + "git.brut.systems/judah/xx/testx" ) func TestBitCast(t *testing.T) { @@ -24,3 +26,53 @@ func TestBitCast(t *testing.T) { t.Fail() } } + +func TestAllocationPrimitives(t *testing.T) { + t.Run("reserve, unreserve", func(t *testing.T) { + data, err := mem.Reserve(1 * mem.Gigabyte) + testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err) + + testx.Expect(t, len(data) == 1*mem.Gigabyte, "len was %d", len(data)) + testx.Expect(t, cap(data) == 1*mem.Gigabyte, "len was %d", cap(data)) + + err = mem.Release(data) + testx.Expect(t, err == nil, "mem.Unreserve returned an error - %s", err) + }) + + t.Run("commit", func(t *testing.T) { + data, err := mem.Reserve(1 * mem.Gigabyte) + testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err) + + err = mem.Commit(data, mem.AccessRead|mem.AccessWrite) + testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err) + + for i := range data { + data[i] = byte(i * i) + } + }) + + t.Run("decommit", func(t *testing.T) { + data, err := mem.Reserve(1 * mem.Gigabyte) + testx.Expect(t, err == nil, "mem.Reserve returned an error - %s", err) + + err = mem.Commit(data, mem.AccessRead|mem.AccessWrite) + testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err) + + before := uintptr(unsafe.Pointer(&data[0])) + + err = mem.Decommit(data) + testx.Expect(t, err == nil, "mem.Decommit returned an error - %s", err) + + // accessing data before recommitting it will fail + + err = mem.Commit(data, mem.AccessRead|mem.AccessWrite) + testx.Expect(t, err == nil, "mem.Commit returned an error - %s", err) + + after := uintptr(unsafe.Pointer(&data[0])) + testx.Expect(t, before == after, "base pointers did not match between after recommit %d != %d", before, after) + + for i := range data { + data[i] = byte(i * i) + } + }) +} diff --git a/mem/mem_unix.go b/mem/mem_unix.go index d85d428..02c0ef8 100644 --- a/mem/mem_unix.go +++ b/mem/mem_unix.go @@ -7,14 +7,7 @@ import ( "unsafe" ) -const ( - PERM_NONE = syscall.PROT_NONE - PERM_READ = syscall.PROT_READ - PERM_WRITE = syscall.PROT_WRITE - PERM_EXECUTE = syscall.PROT_EXEC -) - -func Reserve(total_address_space uintptr) ([]byte, error) { +func reserve(total_address_space uintptr) ([]byte, error) { data, err := syscall.Mmap(-1, 0, int(total_address_space), syscall.PROT_NONE, syscall.MAP_PRIVATE|syscall.MAP_ANON) if err != nil { return nil, err @@ -23,15 +16,15 @@ func Reserve(total_address_space uintptr) ([]byte, error) { return data, nil } -func Unreserve(reserved []byte) error { +func release(reserved []byte) error { return syscall.Munmap(reserved) } -func Commit(reserved []byte, perms int) error { - return syscall.Mprotect(reserved, perms) +func commit(reserved []byte, access Access) error { + return syscall.Mprotect(reserved, access_to_prot(access)) } -func Decommit(committed []byte) (err error) { +func decommit(committed []byte) (err error) { err = syscall.Mprotect(committed, syscall.PROT_NONE) if err != nil { return @@ -57,3 +50,19 @@ func madvise(b []byte, advice int) (err error) { return } + +func access_to_prot(access Access) (prot int) { + prot = syscall.PROT_NONE + + if access&AccessRead != 0 { + prot |= syscall.PROT_READ + } + if access&AccessWrite != 0 { + prot |= syscall.PROT_WRITE + } + if access&AccessExecute != 0 { + prot |= syscall.PROT_EXEC + } + + return +} diff --git a/mem/mem_windows.go b/mem/mem_windows.go new file mode 100644 index 0000000..867c23a --- /dev/null +++ b/mem/mem_windows.go @@ -0,0 +1,88 @@ +//go:build windows + +package mem + +import ( + "syscall" + "unsafe" +) + +func reserve(total_address_space uintptr) ([]byte, error) { + addr, _, err := _VirtualAlloc.Call(0, total_address_space, _MEM_RESERVE, _PAGE_NOACCESS) + if addr == 0 { + return nil, err + } + + return unsafe.Slice((*byte)(unsafe.Pointer(addr)), total_address_space), nil +} + +func release(reserved []byte) error { + res, _, err := _VirtualFree.Call(uintptr(unsafe.Pointer(&reserved[0])), 0, _MEM_RELEASE) + if res == 0 { + return err + } + + return nil +} + +func commit(reserved []byte, access Access) error { + ret, _, err := _VirtualAlloc.Call( + uintptr(unsafe.Pointer(&reserved[0])), + uintptr(len(reserved)), + _MEM_COMMIT, + uintptr(access_to_prot(access)), + ) + if ret == 0 { + return err + } + + return nil +} + +func decommit(committed []byte) error { + ret, _, err := _VirtualFree.Call( + uintptr(unsafe.Pointer(&committed[0])), + uintptr(len(committed)), + _MEM_DECOMMIT, + ) + if ret == 0 { + return err + } + + return nil +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + _VirtualAlloc = kernel32.NewProc("VirtualAlloc") + _VirtualFree = kernel32.NewProc("VirtualFree") +) + +const ( + _MEM_COMMIT = 0x1000 + _MEM_RESERVE = 0x2000 + _MEM_DECOMMIT = 0x4000 + _MEM_RELEASE = 0x8000 + + _PAGE_NOACCESS = 0x01 + _PAGE_READONLY = 0x02 + _PAGE_READWRITE = 0x04 + + _PAGE_EXECUTE_READ = 0x20 + _PAGE_EXECUTE_READWRITE = 0x40 +) + +func access_to_prot(access Access) uint32 { + switch access { + case AccessRead | AccessWrite | AccessExecute: + return _PAGE_EXECUTE_READWRITE + case AccessRead | AccessExecute: + return _PAGE_EXECUTE_READ + case AccessRead | AccessWrite: + return _PAGE_READWRITE + case AccessRead: + return _PAGE_READONLY + default: + return _PAGE_NOACCESS + } +}