feat: overhaul userspace heap and virtual memory
This commit is contained in:
@@ -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