feat: overhaul userspace heap and virtual memory

This commit is contained in:
2026-08-10 12:25:27 +02:00
parent 4ecec32c5c
commit 9b23d99082
21 changed files with 905 additions and 451 deletions
+171
View File
@@ -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);
}