feat: overhaul userspace heap and virtual memory
This commit is contained in:
@@ -229,6 +229,10 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error
|
||||
static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count
|
||||
static constexpr uint64_t SYS_GETCHAR_NB = 166; // () -> ascii, 0 if nothing pending; never blocks
|
||||
static constexpr uint64_t SYS_UTIME = 167;
|
||||
static constexpr uint64_t SYS_MMAP_ANON = 168;
|
||||
static constexpr uint64_t SYS_MUNMAP = 169;
|
||||
static constexpr uint64_t SYS_MPROTECT = 170;
|
||||
|
||||
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
|
||||
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
|
||||
|
||||
@@ -190,6 +190,10 @@ extern "C" {
|
||||
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
|
||||
#define MTK_SYS_NETIFS 165
|
||||
#define MTK_SYS_GETCHAR_NB 166
|
||||
#define MTK_SYS_UTIME 167
|
||||
#define MTK_SYS_MMAP_ANON 168
|
||||
#define MTK_SYS_MUNMAP 169
|
||||
#define MTK_SYS_MPROTECT 170
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
#define MTK_SOCK_TCP 1
|
||||
|
||||
@@ -10,10 +10,8 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Anonymous-memory mmap over SYS_ALLOC. SYS_ALLOC returns
|
||||
* page-aligned process memory, which is exactly what callers like
|
||||
* GCC's page allocator need. File-backed mappings are not
|
||||
* supported and fail with ENODEV.
|
||||
* Demand-paged anonymous memory backed by the kernel's VMA subsystem.
|
||||
* File-backed and shared mappings are not supported and fail cleanly.
|
||||
*/
|
||||
|
||||
#define PROT_NONE 0
|
||||
|
||||
+20
-271
@@ -1,290 +1,39 @@
|
||||
/*
|
||||
* heap.h
|
||||
* Userspace heap allocator for MontaukOS programs
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
* Unified userspace heap API for MontaukOS programs
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// The allocator lives in libc. Keeping these declarations here lets
|
||||
// freestanding C++ programs use the Montauk API without pulling in all of
|
||||
// <stdlib.h>, while ensuring C, C++, and libraries share one heap.
|
||||
extern "C" {
|
||||
void* malloc(std::size_t size);
|
||||
void free(void* ptr);
|
||||
void* realloc(void* ptr, std::size_t size);
|
||||
void* calloc(std::size_t count, std::size_t size);
|
||||
}
|
||||
|
||||
namespace montauk {
|
||||
namespace heap_detail {
|
||||
|
||||
static constexpr uint64_t HEADER_MAGIC = 0x5A484541; // "ZHEA"
|
||||
static constexpr uint64_t FREED_MAGIC = 0xDEADFEEE;
|
||||
|
||||
struct Header {
|
||||
uint64_t magic;
|
||||
uint64_t size; // user-requested size
|
||||
} __attribute__((packed));
|
||||
|
||||
struct FreeNode {
|
||||
uint64_t size; // total size of this free block (including node)
|
||||
FreeNode* next;
|
||||
};
|
||||
|
||||
// Segregated free lists: power-of-2 size classes for blocks <= 4096 bytes.
|
||||
// Blocks larger than 4096 go to the overflow list.
|
||||
static constexpr int NUM_BUCKETS = 8;
|
||||
static constexpr uint64_t BUCKET_SIZES[NUM_BUCKETS] = {
|
||||
32, 64, 128, 256, 512, 1024, 2048, 4096
|
||||
};
|
||||
|
||||
// Per-process heap state — must be `inline` (not `static`) so that all
|
||||
// translation units in a multi-TU program share a single heap.
|
||||
inline FreeNode* g_buckets[NUM_BUCKETS] = {};
|
||||
inline FreeNode g_overflow{0, nullptr};
|
||||
inline bool g_initialized = false;
|
||||
|
||||
// Process-wide heap lock. Userspace threads share the heap, so the
|
||||
// public malloc/mfree/realloc entry points must serialize access to
|
||||
// g_buckets/g_overflow. Kept inline here (not in thread.h) because
|
||||
// thread.h depends on heap.h, and the internal helpers below are not
|
||||
// reentrant into the public API, so a plain spinlock suffices.
|
||||
inline volatile uint32_t g_heap_lock = 0;
|
||||
|
||||
static inline void heap_lock_acquire() {
|
||||
while (__atomic_exchange_n(&g_heap_lock, 1, __ATOMIC_ACQUIRE) != 0) {
|
||||
syscall0(montauk::abi::SYS_YIELD);
|
||||
}
|
||||
}
|
||||
static inline void heap_lock_release() {
|
||||
__atomic_store_n(&g_heap_lock, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static inline Header* get_header(void* block) {
|
||||
return (Header*)((uint8_t*)block - sizeof(Header));
|
||||
}
|
||||
|
||||
// Determine which bucket a block size belongs to, or -1 for overflow
|
||||
static inline int bucket_index(uint64_t blockSize) {
|
||||
if (blockSize <= 32) return 0;
|
||||
if (blockSize <= 64) return 1;
|
||||
if (blockSize <= 128) return 2;
|
||||
if (blockSize <= 256) return 3;
|
||||
if (blockSize <= 512) return 4;
|
||||
if (blockSize <= 1024) return 5;
|
||||
if (blockSize <= 2048) return 6;
|
||||
if (blockSize <= 4096) return 7;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Insert into overflow list (sorted by address, with adjacent-block coalescing)
|
||||
static inline void insert_overflow(void* ptr, uint64_t size) {
|
||||
auto* node = (FreeNode*)ptr;
|
||||
node->size = size;
|
||||
|
||||
FreeNode* prev = &g_overflow;
|
||||
FreeNode* cur = g_overflow.next;
|
||||
while (cur != nullptr && cur < node) {
|
||||
prev = cur;
|
||||
cur = cur->next;
|
||||
}
|
||||
|
||||
bool merged_prev = false;
|
||||
if (prev != &g_overflow &&
|
||||
(uint8_t*)prev + prev->size == (uint8_t*)node) {
|
||||
prev->size += size;
|
||||
node = prev;
|
||||
merged_prev = true;
|
||||
}
|
||||
|
||||
if (cur != nullptr &&
|
||||
(uint8_t*)node + node->size == (uint8_t*)cur) {
|
||||
node->size += cur->size;
|
||||
node->next = cur->next;
|
||||
if (!merged_prev) prev->next = node;
|
||||
} else if (!merged_prev) {
|
||||
node->next = cur;
|
||||
prev->next = node;
|
||||
}
|
||||
}
|
||||
|
||||
// Take a block of at least `needed` bytes from the overflow list.
|
||||
// Splits remainder back into overflow if worthwhile.
|
||||
static inline void* take_from_overflow(uint64_t needed) {
|
||||
FreeNode* prev = &g_overflow;
|
||||
FreeNode* cur = g_overflow.next;
|
||||
|
||||
while (cur != nullptr) {
|
||||
if (cur->size >= needed) {
|
||||
uint64_t blockSize = cur->size;
|
||||
prev->next = cur->next;
|
||||
|
||||
if (blockSize > needed + sizeof(FreeNode) + 16) {
|
||||
insert_overflow((uint8_t*)cur + needed, blockSize - needed);
|
||||
}
|
||||
return (void*)cur;
|
||||
}
|
||||
prev = cur;
|
||||
cur = cur->next;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Next slab size for heap growth. The kernel tracks a finite number
|
||||
// of SYS_ALLOC records per process (MaxHeapAllocs), so growing once
|
||||
// per large allocation exhausts them under allocation-heavy loads
|
||||
// (the native ld ran out mid-link). Doubling slabs keep the syscall
|
||||
// count logarithmic in total heap size.
|
||||
inline uint64_t g_grow_slab = 16 * 0x1000;
|
||||
|
||||
static inline bool grow(uint64_t bytes) {
|
||||
uint64_t want = (bytes + 0xFFF) & ~0xFFFULL;
|
||||
if (want < 0x4000) want = 0x4000;
|
||||
|
||||
uint64_t slab = (want > g_grow_slab) ? want : g_grow_slab;
|
||||
if (g_grow_slab < 4 * 1024 * 1024) g_grow_slab *= 2;
|
||||
|
||||
void* mem = montauk::alloc(slab);
|
||||
if (mem == nullptr && slab > want) {
|
||||
// Big slab refused (low memory): retry with the exact need.
|
||||
slab = want;
|
||||
mem = montauk::alloc(slab);
|
||||
}
|
||||
if (mem == nullptr) return false;
|
||||
insert_overflow(mem, slab);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refill a small-block bucket by carving a page-sized chunk from overflow
|
||||
static inline bool refill_bucket(int idx) {
|
||||
uint64_t bsize = BUCKET_SIZES[idx];
|
||||
uint64_t chunk = (bsize < 4096) ? 4096 : bsize;
|
||||
|
||||
void* block = take_from_overflow(chunk);
|
||||
if (block == nullptr) {
|
||||
if (!grow(chunk)) return false;
|
||||
block = take_from_overflow(chunk);
|
||||
if (block == nullptr) return false;
|
||||
}
|
||||
|
||||
uint64_t count = chunk / bsize;
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
auto* node = (FreeNode*)((uint8_t*)block + i * bsize);
|
||||
node->size = bsize;
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace heap_detail
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
inline void* malloc(uint64_t size) {
|
||||
using namespace heap_detail;
|
||||
|
||||
// Guard against overflow: size + Header must not wrap
|
||||
if (size > UINT64_MAX - sizeof(Header) - 15)
|
||||
return nullptr;
|
||||
|
||||
heap_lock_acquire();
|
||||
|
||||
if (!g_initialized) {
|
||||
grow(16 * 0x1000); // seed with 64 KiB
|
||||
g_initialized = true;
|
||||
}
|
||||
|
||||
uint64_t needed = size + sizeof(Header);
|
||||
needed = (needed + 15) & ~15ULL;
|
||||
|
||||
int idx = bucket_index(needed);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Small allocation — use segregated bucket (O(1))
|
||||
if (g_buckets[idx] == nullptr && !refill_bucket(idx)) {
|
||||
heap_lock_release();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FreeNode* node = g_buckets[idx];
|
||||
g_buckets[idx] = node->next;
|
||||
|
||||
Header* header = (Header*)node;
|
||||
header->magic = HEADER_MAGIC;
|
||||
header->size = size;
|
||||
heap_lock_release();
|
||||
return (void*)((uint8_t*)header + sizeof(Header));
|
||||
}
|
||||
|
||||
// Large allocation — search overflow list
|
||||
void* block = take_from_overflow(needed);
|
||||
if (block == nullptr) {
|
||||
if (!grow(needed)) { heap_lock_release(); return nullptr; }
|
||||
block = take_from_overflow(needed);
|
||||
if (block == nullptr) { heap_lock_release(); return nullptr; }
|
||||
}
|
||||
|
||||
Header* header = (Header*)block;
|
||||
header->magic = HEADER_MAGIC;
|
||||
header->size = size;
|
||||
heap_lock_release();
|
||||
return (void*)((uint8_t*)header + sizeof(Header));
|
||||
return ::malloc((std::size_t)size);
|
||||
}
|
||||
|
||||
inline void mfree(void* ptr) {
|
||||
using namespace heap_detail;
|
||||
|
||||
if (ptr == nullptr) return;
|
||||
|
||||
Header* header = get_header(ptr);
|
||||
|
||||
heap_lock_acquire();
|
||||
|
||||
if (header->magic == FREED_MAGIC) { heap_lock_release(); return; } // double-free
|
||||
if (header->magic != HEADER_MAGIC) { heap_lock_release(); return; } // corrupt
|
||||
header->magic = FREED_MAGIC;
|
||||
|
||||
uint64_t blockSize = header->size + sizeof(Header);
|
||||
blockSize = (blockSize + 15) & ~15ULL;
|
||||
|
||||
int idx = bucket_index(blockSize);
|
||||
|
||||
if (idx >= 0) {
|
||||
// Small block — push onto bucket (O(1))
|
||||
auto* node = (FreeNode*)header;
|
||||
node->size = BUCKET_SIZES[idx];
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
} else {
|
||||
// Large block — sorted insert with coalescing
|
||||
insert_overflow((void*)header, blockSize);
|
||||
}
|
||||
heap_lock_release();
|
||||
::free(ptr);
|
||||
}
|
||||
|
||||
inline void* realloc(void* ptr, uint64_t size) {
|
||||
if (ptr == nullptr) return malloc(size);
|
||||
return ::realloc(ptr, (std::size_t)size);
|
||||
}
|
||||
|
||||
// Read old size under the lock to avoid racing with another
|
||||
// thread that might be freeing/recycling this header.
|
||||
heap_detail::heap_lock_acquire();
|
||||
auto* header = heap_detail::get_header(ptr);
|
||||
uint64_t old = header->size;
|
||||
|
||||
uint64_t oldBlock = (old + sizeof(heap_detail::Header) + 15) & ~15ULL;
|
||||
int idx = heap_detail::bucket_index(oldBlock);
|
||||
if (idx >= 0) oldBlock = heap_detail::BUCKET_SIZES[idx];
|
||||
|
||||
uint64_t newNeed = (size + sizeof(heap_detail::Header) + 15) & ~15ULL;
|
||||
if (newNeed <= oldBlock) {
|
||||
header->size = size;
|
||||
heap_detail::heap_lock_release();
|
||||
return ptr;
|
||||
}
|
||||
heap_detail::heap_lock_release();
|
||||
|
||||
void* newBlock = malloc(size);
|
||||
if (newBlock == nullptr) return nullptr;
|
||||
|
||||
uint64_t copySize = (old < size) ? old : size;
|
||||
memcpy(newBlock, ptr, copySize);
|
||||
|
||||
mfree(ptr);
|
||||
return newBlock;
|
||||
inline void* calloc(uint64_t count, uint64_t size) {
|
||||
return ::calloc((std::size_t)count, (std::size_t)size);
|
||||
}
|
||||
|
||||
} // namespace montauk
|
||||
|
||||
@@ -44,8 +44,22 @@ namespace montauk {
|
||||
ThreadEntry user_entry;
|
||||
void* user_arg;
|
||||
void* stack_base;
|
||||
int tid;
|
||||
ThreadCtx* next;
|
||||
};
|
||||
|
||||
inline ThreadCtx* g_thread_records = nullptr;
|
||||
inline volatile uint32_t g_thread_records_lock = 0;
|
||||
|
||||
inline void records_lock() {
|
||||
while (__atomic_exchange_n(&g_thread_records_lock, 1, __ATOMIC_ACQUIRE) != 0)
|
||||
montauk::yield();
|
||||
}
|
||||
|
||||
inline void records_unlock() {
|
||||
__atomic_store_n(&g_thread_records_lock, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// Userspace trampoline: bridges from the raw entry the kernel jumps
|
||||
// to into the typed entry, then funnels into SYS_THREAD_EXIT. We
|
||||
// route the exit through libc rather than relying on a kernel-side
|
||||
@@ -53,21 +67,24 @@ namespace montauk {
|
||||
// memory on this path.
|
||||
//
|
||||
// The thread's stack itself is intentionally not freed here: we are
|
||||
// still running on it. It is reclaimed when the process exits, or
|
||||
// the joiner may free it explicitly after thread_join.
|
||||
// still running on it. It is reclaimed by a successful thread_join,
|
||||
// or as part of whole-process teardown if the thread is never joined.
|
||||
[[noreturn]] inline void thread_trampoline(detail::ThreadCtx* ctx) {
|
||||
// A sibling CPU can start the thread before thread_spawn has
|
||||
// returned its TID. Wait until the parent has published the record
|
||||
// needed by thread_join to reclaim this stack.
|
||||
while (__atomic_load_n(&ctx->tid, __ATOMIC_ACQUIRE) == 0)
|
||||
montauk::yield();
|
||||
int code = ctx->user_entry(ctx->user_arg);
|
||||
montauk::mfree(ctx);
|
||||
thread_exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn a new thread that begins executing `entry(arg)`. Returns the
|
||||
// new TID on success, or -1 on failure. The thread's stack is
|
||||
// allocated from the user heap; it is leaked on thread exit (the
|
||||
// thread itself cannot free the stack it is running on). The kernel
|
||||
// reclaims it on process exit. Callers that need to spawn many short-
|
||||
// lived threads should pool stacks themselves.
|
||||
// allocated from the user heap. The exiting thread cannot free the stack
|
||||
// it is running on, so thread_join reclaims both it and the trampoline
|
||||
// context after the kernel has reaped the sibling.
|
||||
inline int thread_spawn(ThreadEntry entry, void* arg,
|
||||
uint64_t stack_bytes = 0) {
|
||||
if (entry == nullptr) return -1;
|
||||
@@ -84,6 +101,8 @@ namespace montauk {
|
||||
ctx->user_entry = entry;
|
||||
ctx->user_arg = arg;
|
||||
ctx->stack_base = stack;
|
||||
ctx->tid = 0;
|
||||
ctx->next = nullptr;
|
||||
|
||||
uint64_t stack_top = ((uint64_t)stack + stack_bytes) & ~0xFULL;
|
||||
int tid = (int)syscall3(montauk::abi::SYS_THREAD_SPAWN,
|
||||
@@ -94,6 +113,11 @@ namespace montauk {
|
||||
montauk::mfree(stack);
|
||||
return -1;
|
||||
}
|
||||
detail::records_lock();
|
||||
ctx->next = detail::g_thread_records;
|
||||
detail::g_thread_records = ctx;
|
||||
__atomic_store_n(&ctx->tid, tid, __ATOMIC_RELEASE);
|
||||
detail::records_unlock();
|
||||
return tid;
|
||||
}
|
||||
|
||||
@@ -101,8 +125,22 @@ namespace montauk {
|
||||
// success (with the thread's exit code in *out_code if non-null) or
|
||||
// -1 if `tid` is not a joinable sibling.
|
||||
inline int thread_join(int tid, int* out_code = nullptr) {
|
||||
return (int)syscall2(montauk::abi::SYS_THREAD_JOIN,
|
||||
(uint64_t)tid, (uint64_t)out_code);
|
||||
int result = (int)syscall2(montauk::abi::SYS_THREAD_JOIN,
|
||||
(uint64_t)tid, (uint64_t)out_code);
|
||||
if (result == 0) {
|
||||
detail::records_lock();
|
||||
detail::ThreadCtx** link = &detail::g_thread_records;
|
||||
while (*link != nullptr && (*link)->tid != tid)
|
||||
link = &(*link)->next;
|
||||
detail::ThreadCtx* ctx = *link;
|
||||
if (ctx != nullptr) *link = ctx->next;
|
||||
detail::records_unlock();
|
||||
if (ctx != nullptr) {
|
||||
montauk::mfree(ctx->stack_base);
|
||||
montauk::mfree(ctx);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return the calling thread's TID (== getpid() for the main thread).
|
||||
|
||||
+189
-65
@@ -89,6 +89,7 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
|
||||
/* Syscall numbers */
|
||||
#define SYS_EXIT 0
|
||||
#define SYS_YIELD 1
|
||||
#define SYS_SLEEP_MS 2
|
||||
#define SYS_PRINT 4
|
||||
#define SYS_PUTCHAR 5
|
||||
@@ -119,6 +120,9 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
#define SYS_KILL 62
|
||||
#define SYS_STAT 152
|
||||
#define SYS_UTIME 167
|
||||
#define SYS_MMAP_ANON 168
|
||||
#define SYS_MUNMAP 169
|
||||
#define SYS_MPROTECT 170
|
||||
|
||||
/* ========================================================================
|
||||
errno
|
||||
@@ -626,44 +630,74 @@ int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; }
|
||||
Heap allocator (free-list, backed by SYS_ALLOC)
|
||||
======================================================================== */
|
||||
|
||||
#define HEAP_MAGIC 0x5A484541ULL /* "ZHEA" */
|
||||
#define FREED_MAGIC 0xDEADFEEEULL
|
||||
#define HEAP_MAGIC 0x4D544B4845415041ULL /* "MTKHEAPA" */
|
||||
#define DIRECT_MAGIC 0x4D544B4449524543ULL /* "MTKDIREC" */
|
||||
#define FREED_MAGIC 0x4D544B4652454544ULL /* "MTKFREED" */
|
||||
#define HEAP_ALIGN 16ULL
|
||||
#define DIRECT_THRESHOLD (256ULL * 1024ULL)
|
||||
|
||||
struct HeapHeader {
|
||||
uint64_t magic;
|
||||
uint64_t size;
|
||||
} __attribute__((packed));
|
||||
uint64_t requested_size;
|
||||
uint64_t block_size;
|
||||
uint64_t cookie;
|
||||
};
|
||||
|
||||
struct FreeNode {
|
||||
uint64_t magic;
|
||||
uint64_t size;
|
||||
struct FreeNode *next;
|
||||
};
|
||||
|
||||
/* Segregated free lists: power-of-2 size classes for blocks <= 4096 bytes */
|
||||
#define NUM_BUCKETS 8
|
||||
/* Segregated free lists for small and medium blocks. The minimum class must
|
||||
hold both an allocated header and a free-list node. */
|
||||
#define NUM_BUCKETS 7
|
||||
static const uint64_t BUCKET_SIZES[NUM_BUCKETS] = {
|
||||
32, 64, 128, 256, 512, 1024, 2048, 4096
|
||||
64, 128, 256, 512, 1024, 2048, 4096
|
||||
};
|
||||
|
||||
static struct FreeNode *g_buckets[NUM_BUCKETS] = {};
|
||||
static struct FreeNode g_overflow = { 0, NULL };
|
||||
static struct FreeNode g_overflow = { FREED_MAGIC, 0, NULL };
|
||||
static int g_heapInit = 0;
|
||||
static volatile uint32_t g_heapLock = 0;
|
||||
|
||||
static void heap_lock(void) {
|
||||
while (__atomic_exchange_n(&g_heapLock, 1, __ATOMIC_ACQUIRE) != 0)
|
||||
_zos_syscall0(SYS_YIELD);
|
||||
}
|
||||
|
||||
static void heap_unlock(void) {
|
||||
__atomic_store_n(&g_heapLock, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static int heap_total_size(size_t requested, uint64_t *out) {
|
||||
uint64_t value = (uint64_t)requested;
|
||||
if (value > UINT64_MAX - sizeof(struct HeapHeader) - (HEAP_ALIGN - 1))
|
||||
return 0;
|
||||
*out = (value + sizeof(struct HeapHeader) + (HEAP_ALIGN - 1))
|
||||
& ~(HEAP_ALIGN - 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint64_t heap_cookie(const struct HeapHeader *hdr, uint64_t block_size) {
|
||||
return ((uint64_t)(uintptr_t)hdr >> 4) ^ block_size ^ hdr->magic;
|
||||
}
|
||||
|
||||
static int heap_bucket_index(uint64_t blockSize) {
|
||||
if (blockSize <= 32) return 0;
|
||||
if (blockSize <= 64) return 1;
|
||||
if (blockSize <= 128) return 2;
|
||||
if (blockSize <= 256) return 3;
|
||||
if (blockSize <= 512) return 4;
|
||||
if (blockSize <= 1024) return 5;
|
||||
if (blockSize <= 2048) return 6;
|
||||
if (blockSize <= 4096) return 7;
|
||||
if (blockSize <= 64) return 0;
|
||||
if (blockSize <= 128) return 1;
|
||||
if (blockSize <= 256) return 2;
|
||||
if (blockSize <= 512) return 3;
|
||||
if (blockSize <= 1024) return 4;
|
||||
if (blockSize <= 2048) return 5;
|
||||
if (blockSize <= 4096) return 6;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Insert into overflow list (sorted by address, with coalescing) */
|
||||
static void heap_insert_overflow(void *ptr, uint64_t size) {
|
||||
struct FreeNode *node = (struct FreeNode *)ptr;
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = size;
|
||||
|
||||
struct FreeNode *prev = &g_overflow;
|
||||
@@ -693,7 +727,7 @@ static void heap_insert_overflow(void *ptr, uint64_t size) {
|
||||
}
|
||||
|
||||
/* Take a block >= needed from overflow. Splits remainder back. */
|
||||
static void *heap_take_overflow(uint64_t needed) {
|
||||
static void *heap_take_overflow(uint64_t needed, uint64_t *actual_size) {
|
||||
struct FreeNode *prev = &g_overflow;
|
||||
struct FreeNode *cur = g_overflow.next;
|
||||
|
||||
@@ -702,9 +736,13 @@ static void *heap_take_overflow(uint64_t needed) {
|
||||
uint64_t blockSize = cur->size;
|
||||
prev->next = cur->next;
|
||||
|
||||
if (blockSize > needed + sizeof(struct FreeNode) + 16) {
|
||||
uint64_t min_free = (sizeof(struct FreeNode) + HEAP_ALIGN - 1)
|
||||
& ~(HEAP_ALIGN - 1);
|
||||
if (blockSize >= needed + min_free) {
|
||||
heap_insert_overflow((uint8_t *)cur + needed, blockSize - needed);
|
||||
blockSize = needed;
|
||||
}
|
||||
*actual_size = blockSize;
|
||||
return (void *)cur;
|
||||
}
|
||||
prev = cur;
|
||||
@@ -713,11 +751,8 @@ static void *heap_take_overflow(uint64_t needed) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Next slab size for heap growth. The kernel tracks a finite number of
|
||||
SYS_ALLOC records per process (MaxHeapAllocs), so growing once per
|
||||
large allocation exhausts them: ld ran out mid-link and BFD reported
|
||||
the resulting NULL mallocs as "file format not recognized". Doubling
|
||||
slabs keep the syscall count logarithmic in total heap size. */
|
||||
/* Next slab size for heap growth. Doubling slabs keeps syscall and VMA
|
||||
metadata traffic logarithmic in total heap size. */
|
||||
static uint64_t g_heap_slab = 16 * 0x1000;
|
||||
|
||||
static void heap_grow(uint64_t bytes) {
|
||||
@@ -742,16 +777,24 @@ static int heap_refill_bucket(int idx) {
|
||||
uint64_t bsize = BUCKET_SIZES[idx];
|
||||
uint64_t chunk = (bsize < 4096) ? 4096 : bsize;
|
||||
|
||||
void *block = heap_take_overflow(chunk);
|
||||
uint64_t actual = 0;
|
||||
void *block = heap_take_overflow(chunk, &actual);
|
||||
if (block == NULL) {
|
||||
heap_grow(chunk);
|
||||
block = heap_take_overflow(chunk);
|
||||
block = heap_take_overflow(chunk, &actual);
|
||||
if (block == NULL) return 0;
|
||||
}
|
||||
|
||||
/* A small unsplittable tail belongs to the overflow list, rather than
|
||||
disappearing when the main chunk is carved into bucket blocks. */
|
||||
if (actual - chunk >= ((sizeof(struct FreeNode) + HEAP_ALIGN - 1)
|
||||
& ~(HEAP_ALIGN - 1)))
|
||||
heap_insert_overflow((uint8_t *)block + chunk, actual - chunk);
|
||||
|
||||
uint64_t count = chunk / bsize;
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
struct FreeNode *node = (struct FreeNode *)((uint8_t *)block + i * bsize);
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = bsize;
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
@@ -759,19 +802,31 @@ static int heap_refill_bucket(int idx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *malloc(size_t size) {
|
||||
static void *heap_malloc_locked(size_t size) {
|
||||
uint64_t needed;
|
||||
if (!heap_total_size(size, &needed))
|
||||
return NULL;
|
||||
|
||||
/* Large objects get their own page mapping, so free can promptly return
|
||||
both physical memory and virtual space to the kernel. */
|
||||
if (needed >= DIRECT_THRESHOLD) {
|
||||
if (needed > UINT64_MAX - 0xFFFULL) return NULL;
|
||||
uint64_t mapping_size = (needed + 0xFFFULL) & ~0xFFFULL;
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)
|
||||
_zos_syscall1(SYS_ALLOC, (long)mapping_size);
|
||||
if (hdr == NULL) return NULL;
|
||||
hdr->magic = DIRECT_MAGIC;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = mapping_size;
|
||||
hdr->cookie = heap_cookie(hdr, mapping_size);
|
||||
return (uint8_t *)hdr + sizeof(*hdr);
|
||||
}
|
||||
|
||||
if (!g_heapInit) {
|
||||
heap_grow(16 * 0x1000);
|
||||
g_heapInit = 1;
|
||||
}
|
||||
|
||||
/* Guard against overflow: size + Header must not wrap */
|
||||
if (size > (uint64_t)-1 - sizeof(struct HeapHeader) - 15)
|
||||
return NULL;
|
||||
|
||||
uint64_t needed = size + sizeof(struct HeapHeader);
|
||||
needed = (needed + 15) & ~15ULL;
|
||||
|
||||
int idx = heap_bucket_index(needed);
|
||||
|
||||
if (idx >= 0) {
|
||||
@@ -781,44 +836,63 @@ void *malloc(size_t size) {
|
||||
|
||||
struct FreeNode *node = g_buckets[idx];
|
||||
g_buckets[idx] = node->next;
|
||||
uint64_t block_size = node->size;
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)node;
|
||||
hdr->magic = HEAP_MAGIC;
|
||||
hdr->size = size;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = block_size;
|
||||
hdr->cookie = heap_cookie(hdr, hdr->block_size);
|
||||
return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader));
|
||||
}
|
||||
|
||||
/* Large allocation — search overflow list */
|
||||
void *block = heap_take_overflow(needed);
|
||||
uint64_t actual = 0;
|
||||
void *block = heap_take_overflow(needed, &actual);
|
||||
if (block == NULL) {
|
||||
heap_grow(needed);
|
||||
block = heap_take_overflow(needed);
|
||||
block = heap_take_overflow(needed, &actual);
|
||||
if (block == NULL) return NULL;
|
||||
}
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)block;
|
||||
hdr->magic = HEAP_MAGIC;
|
||||
hdr->size = size;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = actual;
|
||||
hdr->cookie = heap_cookie(hdr, actual);
|
||||
return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader));
|
||||
}
|
||||
|
||||
void free(void *ptr) {
|
||||
static int heap_header_valid(const struct HeapHeader *hdr) {
|
||||
return (hdr->magic == HEAP_MAGIC || hdr->magic == DIRECT_MAGIC) &&
|
||||
hdr->block_size >= sizeof(struct HeapHeader) &&
|
||||
(hdr->block_size & (HEAP_ALIGN - 1)) == 0 &&
|
||||
hdr->cookie == heap_cookie(hdr, hdr->block_size) &&
|
||||
hdr->requested_size <= hdr->block_size - sizeof(struct HeapHeader);
|
||||
}
|
||||
|
||||
static void heap_free_locked(void *ptr) {
|
||||
if (ptr == NULL) return;
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader));
|
||||
|
||||
if (hdr->magic == FREED_MAGIC) return; /* double-free */
|
||||
if (hdr->magic != HEAP_MAGIC) return; /* corrupt */
|
||||
if (!heap_header_valid(hdr)) return;
|
||||
|
||||
uint64_t blockSize = hdr->block_size;
|
||||
int direct = hdr->magic == DIRECT_MAGIC;
|
||||
hdr->magic = FREED_MAGIC;
|
||||
|
||||
uint64_t blockSize = hdr->size + sizeof(struct HeapHeader);
|
||||
blockSize = (blockSize + 15) & ~15ULL;
|
||||
if (direct) {
|
||||
_zos_syscall1(SYS_FREE, (long)hdr);
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = heap_bucket_index(blockSize);
|
||||
|
||||
if (idx >= 0) {
|
||||
/* Small block — push onto bucket (O(1)) */
|
||||
struct FreeNode *node = (struct FreeNode *)hdr;
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = BUCKET_SIZES[idx];
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
@@ -828,6 +902,21 @@ void free(void *ptr) {
|
||||
}
|
||||
}
|
||||
|
||||
void *malloc(size_t size) {
|
||||
void *result;
|
||||
heap_lock();
|
||||
result = heap_malloc_locked(size);
|
||||
heap_unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void free(void *ptr) {
|
||||
if (ptr == NULL) return;
|
||||
heap_lock();
|
||||
heap_free_locked(ptr);
|
||||
heap_unlock();
|
||||
}
|
||||
|
||||
void *calloc(size_t nmemb, size_t size) {
|
||||
/* Check for multiplication overflow */
|
||||
if (nmemb != 0 && size > (size_t)-1 / nmemb)
|
||||
@@ -842,26 +931,39 @@ void *realloc(void *ptr, size_t size) {
|
||||
if (ptr == NULL) return malloc(size);
|
||||
if (size == 0) { free(ptr); return NULL; }
|
||||
|
||||
uint64_t newNeed;
|
||||
if (!heap_total_size(size, &newNeed))
|
||||
return NULL;
|
||||
|
||||
heap_lock();
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader));
|
||||
uint64_t old = hdr->size;
|
||||
if (!heap_header_valid(hdr)) {
|
||||
heap_unlock();
|
||||
return NULL;
|
||||
}
|
||||
uint64_t old = hdr->requested_size;
|
||||
uint64_t oldBlock = hdr->block_size;
|
||||
|
||||
/* Compute actual block size (accounting for bucket rounding) */
|
||||
uint64_t oldBlock = (old + sizeof(struct HeapHeader) + 15) & ~15ULL;
|
||||
int idx = heap_bucket_index(oldBlock);
|
||||
if (idx >= 0) oldBlock = BUCKET_SIZES[idx];
|
||||
|
||||
uint64_t newNeed = (size + sizeof(struct HeapHeader) + 15) & ~15ULL;
|
||||
if (newNeed <= oldBlock) {
|
||||
hdr->size = size;
|
||||
/* Retain the actual extent. Losing it here makes the tail impossible
|
||||
to recover when this block is later freed. */
|
||||
hdr->requested_size = size;
|
||||
hdr->cookie = heap_cookie(hdr, oldBlock);
|
||||
heap_unlock();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void *newp = malloc(size);
|
||||
if (newp == NULL) return NULL;
|
||||
void *newp = heap_malloc_locked(size);
|
||||
if (newp == NULL) {
|
||||
heap_unlock();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t copySize = old < size ? old : size;
|
||||
memcpy(newp, ptr, copySize);
|
||||
free(ptr);
|
||||
heap_free_locked(ptr);
|
||||
heap_unlock();
|
||||
return newp;
|
||||
}
|
||||
|
||||
@@ -3874,17 +3976,26 @@ long sysconf(int name) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Anonymous mappings only: SYS_ALLOC hands back zeroed page-aligned
|
||||
memory and SYS_FREE releases it. Length is remembered by the kernel
|
||||
per allocation, so munmap ignores its length argument. */
|
||||
/* Anonymous private mappings with kernel-enforced page permissions. */
|
||||
void *mmap(void *addr, size_t length, int prot, int flags, int fd,
|
||||
long offset) {
|
||||
(void)addr; (void)prot; (void)offset;
|
||||
if (length == 0 || fd != -1 || !(flags & MAP_ANONYMOUS)) {
|
||||
errno = ENODEV;
|
||||
int supported_flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||
if (length == 0 || fd != -1 || offset != 0 ||
|
||||
(flags & supported_flags) != supported_flags ||
|
||||
(flags & ~supported_flags) != 0) {
|
||||
errno = EINVAL;
|
||||
return MAP_FAILED;
|
||||
}
|
||||
void *p = (void *)_zos_syscall1(SYS_ALLOC, (long)length);
|
||||
/* Address hints may be ignored; fixed placement is intentionally rejected
|
||||
by the flag validation above. */
|
||||
(void)addr;
|
||||
if ((prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC)) != 0 ||
|
||||
!(prot & PROT_READ) ||
|
||||
((prot & PROT_WRITE) && (prot & PROT_EXEC))) {
|
||||
errno = EINVAL;
|
||||
return MAP_FAILED;
|
||||
}
|
||||
void *p = (void *)_zos_syscall2(SYS_MMAP_ANON, (long)length, (long)prot);
|
||||
if (p == NULL) {
|
||||
errno = ENOMEM;
|
||||
return MAP_FAILED;
|
||||
@@ -3893,18 +4004,31 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd,
|
||||
}
|
||||
|
||||
int munmap(void *addr, size_t length) {
|
||||
(void)length;
|
||||
if (addr == NULL || addr == MAP_FAILED) {
|
||||
if (addr == NULL || addr == MAP_FAILED || length == 0 ||
|
||||
((uintptr_t)addr & 0xFFFULL) != 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (_zos_syscall2(SYS_MUNMAP, (long)addr, (long)length) < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
_zos_syscall1(SYS_FREE, (long)addr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t length, int prot) {
|
||||
(void)addr; (void)length; (void)prot;
|
||||
return 0; /* page protections are not adjustable from userspace */
|
||||
if (addr == NULL || length == 0 || ((uintptr_t)addr & 0xFFFULL) != 0 ||
|
||||
(prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC)) != 0 ||
|
||||
!(prot & PROT_READ) ||
|
||||
((prot & PROT_WRITE) && (prot & PROT_EXEC))) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (_zos_syscall3(SYS_MPROTECT, (long)addr, (long)length, (long)prot) < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long pathconf(const char *path, int name) {
|
||||
|
||||
Binary file not shown.
+20
-18
@@ -9,49 +9,51 @@
|
||||
|
||||
.SH DESCRIPTION
|
||||
The userspace heap provides dynamic memory allocation on top of
|
||||
the kernel's page-mapping syscall (SYS_ALLOC). Include the
|
||||
header <montauk/heap.h> to use these functions.
|
||||
anonymous virtual memory. The Montauk C++ API and libc's
|
||||
malloc/free API use the same process-wide allocator.
|
||||
|
||||
.SS malloc
|
||||
Allocates 'size' bytes from the free list. Returns a 16-byte
|
||||
aligned pointer, or nullptr on failure. When the free list is
|
||||
empty, it requests more pages from the kernel via SYS_ALLOC
|
||||
(minimum 16 KiB growth, initial seed of 64 KiB).
|
||||
empty, it reserves more pages from the kernel. Physical pages
|
||||
are committed as they are first touched.
|
||||
|
||||
char* buf = (char*)montauk::malloc(1024);
|
||||
|
||||
.SS mfree
|
||||
Returns the block to the userspace free list. No syscall is
|
||||
made -- the memory stays mapped and is immediately reusable.
|
||||
Returns the block to the userspace allocator. Arena blocks are
|
||||
immediately reusable; large direct mappings are returned to the
|
||||
kernel, including their virtual address range.
|
||||
Passing nullptr is a safe no-op.
|
||||
|
||||
montauk::mfree(buf);
|
||||
|
||||
.SS realloc
|
||||
Resizes the allocation to 'size' bytes. Allocates a new block,
|
||||
copies the smaller of old/new sizes, and frees the old block.
|
||||
Resizes the allocation to 'size' bytes. A block with sufficient
|
||||
capacity is retained; otherwise a new block is allocated, the
|
||||
smaller of old/new requested sizes is copied, and the old block
|
||||
is freed. Integer overflow fails without changing the old block.
|
||||
If ptr is nullptr, behaves like malloc.
|
||||
|
||||
buf = (char*)montauk::realloc(buf, 2048);
|
||||
|
||||
.SH IMPLEMENTATION
|
||||
The allocator uses a linked free-list with first-fit search.
|
||||
Blocks larger than needed are split. The allocation header is
|
||||
16 bytes (magic + size). All allocations are 16-byte aligned.
|
||||
The allocator uses segregated size-class bins and a coalescing
|
||||
address-ordered overflow list. Headers retain both requested size
|
||||
and actual block extent. A process-wide lock serializes C and C++
|
||||
allocation calls. All returned pointers are 16-byte aligned.
|
||||
|
||||
The heap grows by requesting pages from the kernel via
|
||||
SYS_ALLOC. These pages are never returned to the kernel (since
|
||||
SYS_FREE is currently a no-op), but mfree makes them available
|
||||
for future malloc calls within the process.
|
||||
Allocations of 256 KiB or more use direct page mappings so they
|
||||
can be released promptly. Smaller allocations use growing arenas.
|
||||
|
||||
.SH LOW-LEVEL PAGE API
|
||||
For large allocations or when direct page control is needed:
|
||||
|
||||
void* montauk::alloc(uint64_t size); // SYS_ALLOC
|
||||
void montauk::free(void* ptr); // SYS_FREE (no-op)
|
||||
void montauk::free(void* ptr); // SYS_FREE
|
||||
|
||||
alloc() maps zeroed pages starting at 0x40000000 and growing
|
||||
upward. Size is rounded up to 4 KiB page boundaries.
|
||||
alloc() reserves zero-filled, read/write, non-executable pages.
|
||||
Size is rounded up to 4 KiB. Freed ranges are reusable.
|
||||
|
||||
.SH SEE ALSO
|
||||
syscalls(2), file(2)
|
||||
|
||||
+16
-1
@@ -185,9 +185,24 @@
|
||||
void* montauk::alloc(uint64_t size);
|
||||
|
||||
.B SYS_FREE (12)
|
||||
Reserved (currently a no-op).
|
||||
Release a complete mapping previously returned by SYS_ALLOC.
|
||||
void montauk::free(void* ptr);
|
||||
|
||||
.B SYS_MMAP_ANON (168)
|
||||
Reserve a zero-filled anonymous mapping with read/write/execute
|
||||
protection flags. Pages are committed on first access. Writable
|
||||
executable mappings are rejected.
|
||||
void* mmap(void*, size_t, int, int, int, long);
|
||||
|
||||
.B SYS_MUNMAP (169)
|
||||
Release a page-aligned range. Partial unmap splits the VM area and
|
||||
makes the virtual range reusable.
|
||||
int munmap(void* addr, size_t length);
|
||||
|
||||
.B SYS_MPROTECT (170)
|
||||
Change read/write/execute permissions on an anonymous mapping.
|
||||
int mprotect(void* addr, size_t length, int prot);
|
||||
|
||||
.B SYS_MEMSTATS (67)
|
||||
Get kernel-wide physical memory usage (total/free/used bytes,
|
||||
page size).
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* memtest - userspace heap and virtual-memory regression tests
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/thread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
namespace {
|
||||
int failures = 0;
|
||||
|
||||
void check(bool condition, const char* name) {
|
||||
montauk::print(condition ? "PASS " : "FAIL ");
|
||||
montauk::print(name);
|
||||
montauk::putchar('\n');
|
||||
if (!condition) failures++;
|
||||
}
|
||||
|
||||
bool bytes_are(const uint8_t* p, size_t n, uint8_t value) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (p[i] != value) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct WorkerArgs {
|
||||
volatile uint32_t* failures;
|
||||
uint8_t seed;
|
||||
};
|
||||
|
||||
int heap_worker(void* opaque) {
|
||||
auto* args = (WorkerArgs*)opaque;
|
||||
for (int round = 0; round < 256; round++) {
|
||||
size_t size = (size_t)((round * 37 + args->seed) % 8192 + 1);
|
||||
auto* p = (uint8_t*)montauk::malloc(size);
|
||||
if (p == nullptr) {
|
||||
__atomic_fetch_add(args->failures, 1, __ATOMIC_RELAXED);
|
||||
continue;
|
||||
}
|
||||
uint8_t value = (uint8_t)(args->seed + round);
|
||||
for (size_t i = 0; i < size; i++) p[i] = value;
|
||||
size_t grown = size + (size_t)(round % 97);
|
||||
auto* q = (uint8_t*)montauk::realloc(p, grown);
|
||||
if (q == nullptr || !bytes_are(q, size, value)) {
|
||||
__atomic_fetch_add(args->failures, 1, __ATOMIC_RELAXED);
|
||||
if (q != nullptr) montauk::mfree(q);
|
||||
else montauk::mfree(p);
|
||||
continue;
|
||||
}
|
||||
montauk::mfree(q);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
// Alignment, reuse, and data integrity across all current size classes.
|
||||
void* blocks[512] = {};
|
||||
bool basic_ok = true;
|
||||
int block_count = 0;
|
||||
for (int i = 0; i < 512; i++) {
|
||||
size_t size = (size_t)((i * 53) % 12000);
|
||||
blocks[i] = montauk::malloc(size);
|
||||
if (blocks[i] == nullptr || ((uintptr_t)blocks[i] & 15) != 0) {
|
||||
basic_ok = false;
|
||||
break;
|
||||
}
|
||||
block_count++;
|
||||
uint8_t value = (uint8_t)i;
|
||||
for (size_t j = 0; j < size; j++) ((uint8_t*)blocks[i])[j] = value;
|
||||
}
|
||||
for (int i = 0; i < block_count; i += 2) {
|
||||
montauk::mfree(blocks[i]);
|
||||
blocks[i] = nullptr;
|
||||
}
|
||||
for (int i = 1; i < block_count; i += 2) {
|
||||
size_t size = (size_t)((i * 53) % 12000);
|
||||
if (!bytes_are((uint8_t*)blocks[i], size, (uint8_t)i)) basic_ok = false;
|
||||
montauk::mfree(blocks[i]);
|
||||
}
|
||||
check(basic_ok, "size classes preserve data and 16-byte alignment");
|
||||
|
||||
auto* overflow = (uint8_t*)montauk::malloc(64);
|
||||
for (int i = 0; i < 64; i++) overflow[i] = 0xA5;
|
||||
void* rejected = montauk::realloc(overflow, UINT64_MAX);
|
||||
check(rejected == nullptr && bytes_are(overflow, 64, 0xA5),
|
||||
"realloc overflow fails without altering the old allocation");
|
||||
montauk::mfree(overflow);
|
||||
|
||||
auto* shrink = (uint8_t*)montauk::malloc(32000);
|
||||
for (int i = 0; i < 32000; i++) shrink[i] = (uint8_t)i;
|
||||
auto* shrunk = (uint8_t*)montauk::realloc(shrink, 4097);
|
||||
check(shrunk == shrink, "large in-arena realloc shrink retains its extent");
|
||||
// The first byte sequence is not uniformly zero; validate explicitly.
|
||||
bool shrink_data_ok = true;
|
||||
for (int i = 0; i < 4097; i++)
|
||||
if (shrunk[i] != (uint8_t)i) { shrink_data_ok = false; break; }
|
||||
check(shrink_data_ok, "realloc shrink preserves payload");
|
||||
montauk::mfree(shrunk);
|
||||
|
||||
auto* direct1 = (uint8_t*)montauk::malloc(512 * 1024);
|
||||
uintptr_t direct_addr = (uintptr_t)direct1;
|
||||
if (direct1) direct1[511 * 1024] = 0x6D;
|
||||
montauk::mfree(direct1);
|
||||
auto* direct2 = (uint8_t*)montauk::malloc(512 * 1024);
|
||||
check(direct2 != nullptr && (uintptr_t)direct2 == direct_addr,
|
||||
"large allocation releases and reuses its VM range");
|
||||
|
||||
auto* raw1 = (uint8_t*)montauk::alloc(3 * 4096);
|
||||
uintptr_t raw_addr = (uintptr_t)raw1;
|
||||
bool raw_split = raw1 != nullptr &&
|
||||
mprotect(raw1, 4096, PROT_READ) == 0;
|
||||
montauk::free(raw1);
|
||||
auto* raw2 = (uint8_t*)montauk::alloc(3 * 4096);
|
||||
check(raw_split && raw2 != nullptr && (uintptr_t)raw2 == raw_addr,
|
||||
"SYS_FREE releases every fragment after mprotect splitting");
|
||||
|
||||
auto* map = (uint8_t*)mmap(nullptr, 3 * 4096, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
bool map_ok = map != MAP_FAILED && ((uintptr_t)map & 0xFFF) == 0 &&
|
||||
bytes_are(map, 3 * 4096, 0);
|
||||
check(map_ok, "anonymous mappings are aligned and zero-filled");
|
||||
if (map != MAP_FAILED) {
|
||||
map[0] = 1;
|
||||
map[8192] = 2;
|
||||
check(mprotect(map, 4096, PROT_READ) == 0 &&
|
||||
mprotect(map, 4096, PROT_READ | PROT_WRITE) == 0,
|
||||
"mprotect changes mapped page permissions");
|
||||
check(mprotect(map, 3 * 4096, PROT_READ) == 0 &&
|
||||
mprotect(map, 3 * 4096, PROT_READ | PROT_WRITE) == 0,
|
||||
"mprotect spans adjacent VMA fragments");
|
||||
check(mprotect(map, 4096, PROT_READ | PROT_WRITE | PROT_EXEC) < 0,
|
||||
"mprotect rejects writable executable memory");
|
||||
void* middle = map + 4096;
|
||||
check(munmap(middle, 4096) == 0, "munmap supports VMA splitting");
|
||||
void* replacement = mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
check(replacement == middle, "munmap makes virtual ranges reusable");
|
||||
if (replacement != MAP_FAILED) munmap(replacement, 4096);
|
||||
munmap(map, 4096);
|
||||
munmap(map + 8192, 4096);
|
||||
}
|
||||
// Keep this extent occupied until the partial-unmap test above has
|
||||
// verified that its own hole is the first reusable range.
|
||||
montauk::free(raw2);
|
||||
montauk::mfree(direct2);
|
||||
|
||||
volatile uint32_t worker_failures = 0;
|
||||
WorkerArgs args[4] = {};
|
||||
int tids[4] = {};
|
||||
bool threads_ok = true;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
args[i] = { &worker_failures, (uint8_t)(17 + i * 31) };
|
||||
tids[i] = montauk::thread_spawn(heap_worker, &args[i]);
|
||||
if (tids[i] < 0) threads_ok = false;
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int code = -1;
|
||||
if (tids[i] >= 0 && (montauk::thread_join(tids[i], &code) < 0 || code != 0))
|
||||
threads_ok = false;
|
||||
}
|
||||
check(threads_ok && worker_failures == 0,
|
||||
"concurrent malloc/realloc/free stress");
|
||||
|
||||
montauk::print(failures == 0 ? "memtest: all tests passed\n"
|
||||
: "memtest: failures detected\n");
|
||||
montauk::exit(failures == 0 ? 0 : 1);
|
||||
}
|
||||
Reference in New Issue
Block a user