diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index c91ba51..aa327e8 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 95 +#define MONTAUK_BUILD_NUMBER 96 diff --git a/kernel/src/Api/Heap.hpp b/kernel/src/Api/Heap.hpp index 5fa6476..cfec302 100644 --- a/kernel/src/Api/Heap.hpp +++ b/kernel/src/Api/Heap.hpp @@ -4,6 +4,7 @@ * Copyright (c) 2026 Daniel Hammer */ +#pragma once #include #include #include @@ -18,12 +19,16 @@ namespace montauk::abi { struct HeapAlloc { uint64_t va; uint64_t numPages; + uint64_t prot; + uint64_t allocationId; + HeapAlloc* next; }; - static constexpr int MaxHeapAllocs = 512; - - inline HeapAlloc g_heapAllocs[Sched::MaxProcesses][MaxHeapAllocs] = {}; - inline int g_heapAllocCount[Sched::MaxProcesses] = {}; + // VM area metadata is dynamically sized. The previous 512-entry array made + // valid mappings fail for metadata exhaustion and consumed several MiB of + // kernel BSS even for processes with no mappings. + inline HeapAlloc* g_heapAllocs[Sched::MaxProcesses] = {}; + inline uint64_t g_nextHeapAllocationId[Sched::MaxProcesses] = {}; inline kcp::Mutex g_heapLocks[Sched::MaxProcesses]; // Get the process table slot index for the current process @@ -35,12 +40,22 @@ namespace montauk::abi { return (int)(proc - slot0); } - inline uint64_t Sys_Alloc(uint64_t size) { + static constexpr uint64_t VmProtRead = 1; + static constexpr uint64_t VmProtWrite = 2; + static constexpr uint64_t VmProtExec = 4; + + inline uint64_t Sys_MapAnonymous(uint64_t size, uint64_t prot) { auto* proc = Sched::GetCurrentProcessPtr(); if (proc == nullptr) return 0; int slot = GetCurrentSlot(); if (slot < 0) return 0; + if ((prot & ~(VmProtRead | VmProtWrite | VmProtExec)) != 0 || + (prot & VmProtRead) == 0 || + ((prot & VmProtWrite) != 0 && (prot & VmProtExec) != 0)) { + return 0; + } + // Guard against overflow before rounding. if (size > 0xFFFFFFFFFFFF0000ULL) return 0; @@ -50,17 +65,6 @@ namespace montauk::abi { uint64_t numPages = size / 0x1000; g_heapLocks[slot].Acquire(); - if (g_heapAllocCount[slot] >= MaxHeapAllocs) { - // Out of allocation records, not out of memory. Log it: a - // silent 0 here surfaced as bogus downstream errors (BFD - // turned NULL mallocs into "file format not recognized"). - Kt::KernelLogStream(Kt::ERROR, "Heap") - << "pid " << proc->pid << " (" << proc->name - << ") hit MaxHeapAllocs (" << (uint64_t)MaxHeapAllocs - << "), SYS_ALLOC refused"; - g_heapLocks[slot].Release(); - return 0; - } uint64_t userVa = 0; if (!Sched::ReserveUserHeapRange(slot, size, userVa)) { @@ -71,39 +75,34 @@ namespace montauk::abi { return 0; } - // Allocate physical pages and map them into the process - uint64_t mappedPages = 0; - for (uint64_t i = 0; i < numPages; i++) { - void* page = Memory::g_pfa->AllocateZeroed(); - if (page == nullptr) { - Ipc::UnmapAndFreeUserRange(proc->pml4Phys, userVa, mappedPages); - g_heapLocks[slot].Release(); - return 0; - } - uint64_t physAddr = Memory::SubHHDM((uint64_t)page); - if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa + i * 0x1000)) { - Memory::g_pfa->Free(page); - Ipc::UnmapAndFreeUserRange(proc->pml4Phys, userVa, mappedPages); - g_heapLocks[slot].Release(); - return 0; - } - mappedPages++; - } - - // Track the allocation so Sys_Free can release it - Sched::g_allocatedPages[slot] += numPages; - g_heapAllocs[slot][g_heapAllocCount[slot]++] = { userVa, numPages }; + // Reserve a VMA without committing physical pages. First access is + // satisfied by TryHandleAnonymousPageFault with a zeroed frame. + uint64_t allocationId = ++g_nextHeapAllocationId[slot]; + if (allocationId == 0) allocationId = ++g_nextHeapAllocationId[slot]; + g_heapAllocs[slot] = new HeapAlloc { userVa, numPages, prot, allocationId, + g_heapAllocs[slot] }; g_heapLocks[slot].Release(); return userVa; } + inline uint64_t Sys_Alloc(uint64_t size) { + return Sys_MapAnonymous(size, VmProtRead | VmProtWrite); + } + // Reset heap allocation tracking for a process slot. // The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup. inline void CleanupHeapForSlot(int slot, uint64_t /*pml4Phys*/) { if (slot < 0 || slot >= Sched::MaxProcesses) return; g_heapLocks[slot].Acquire(); - g_heapAllocCount[slot] = 0; + HeapAlloc* area = g_heapAllocs[slot]; + while (area != nullptr) { + HeapAlloc* next = area->next; + delete area; + area = next; + } + g_heapAllocs[slot] = nullptr; + g_nextHeapAllocationId[slot] = 0; Sched::g_allocatedPages[slot] = 0; g_heapLocks[slot].Release(); } @@ -117,31 +116,245 @@ namespace montauk::abi { g_heapLocks[slot].Acquire(); - // Find the allocation record matching this address - int idx = -1; - for (int i = 0; i < g_heapAllocCount[slot]; i++) { - if (g_heapAllocs[slot][i].va == addr) { - idx = i; + // A protection change can split one allocation into several VMA + // records. Resolve the stable allocation ID from the supplied base, + // detach every surviving fragment, then tear it down without holding + // the VMA lock. + HeapAlloc* released = nullptr; + HeapAlloc** link = &g_heapAllocs[slot]; + uint64_t resident = 0; + uint64_t allocationId = 0; + for (HeapAlloc* area = g_heapAllocs[slot]; area != nullptr; + area = area->next) { + if (area->va == addr) { + allocationId = area->allocationId; break; } } - if (idx < 0) { + if (allocationId == 0) { g_heapLocks[slot].Release(); return; // Unknown address — ignore } + while (*link != nullptr) { + HeapAlloc* area = *link; + if (area->allocationId != allocationId) { + link = &area->next; + continue; + } + for (uint64_t i = 0; i < area->numPages; i++) + if (Memory::VMM::Paging::GetPhysAddr( + proc->pml4Phys, area->va + i * 0x1000ULL) != 0) + resident++; + *link = area->next; + area->next = released; + released = area; + } + g_heapLocks[slot].Release(); - uint64_t va = g_heapAllocs[slot][idx].va; - uint64_t numPages = g_heapAllocs[slot][idx].numPages; + // Unmap and invalidate sibling CPUs before recycling frames. A stale + // user TLB entry can otherwise corrupt the frame's next owner. + while (released != nullptr) { + HeapAlloc* next = released->next; + Ipc::UnmapAndFreeUserRange(proc->pml4Phys, released->va, + released->numPages); + Sched::ReleaseUserHeapRange(slot, released->va, + released->numPages * 0x1000ULL); + delete released; + released = next; + } - // Unmap and invalidate sibling CPUs before recycling the frames. A - // stale user TLB entry can otherwise corrupt the frame's next owner. - Ipc::UnmapAndFreeUserRange(proc->pml4Phys, va, numPages); - - Sched::g_allocatedPages[slot] -= numPages; - - // Remove tracking entry by swapping with the last element - g_heapAllocs[slot][idx] = g_heapAllocs[slot][g_heapAllocCount[slot] - 1]; - g_heapAllocCount[slot]--; + g_heapLocks[slot].Acquire(); + Sched::g_allocatedPages[slot] -= resident; g_heapLocks[slot].Release(); } + + inline int64_t Sys_Unmap(uint64_t addr, uint64_t size) { + auto* proc = Sched::GetCurrentProcessPtr(); + int slot = GetCurrentSlot(); + if (proc == nullptr || slot < 0 || size == 0 || (addr & 0xFFFULL) != 0 || + size > UINT64_MAX - 0xFFFULL) return -1; + size = (size + 0xFFFULL) & ~0xFFFULL; + if (addr > UINT64_MAX - size) return -1; + + g_heapLocks[slot].Acquire(); + uint64_t firstPage = 0; + uint64_t pages = size / 0x1000ULL; + HeapAlloc** link = &g_heapAllocs[slot]; + while (*link != nullptr) { + HeapAlloc* area = *link; + uint64_t areaSize = area->numPages * 0x1000ULL; + if (addr >= area->va && addr - area->va <= areaSize && + size <= areaSize - (addr - area->va)) { + firstPage = (addr - area->va) / 0x1000ULL; + break; + } + link = &area->next; + } + if (*link == nullptr) { g_heapLocks[slot].Release(); return -1; } + + HeapAlloc* area = *link; + uint64_t oldPages = area->numPages; + uint64_t oldProt = area->prot; + uint64_t tailPages = oldPages - firstPage - pages; + + if (firstPage == 0 && tailPages == 0) { + *link = area->next; + delete area; + } else if (firstPage == 0) { + area->va = addr + size; + area->numPages = tailPages; + } else { + area->numPages = firstPage; + if (tailPages != 0) { + area->next = new HeapAlloc { addr + size, tailPages, oldProt, + area->allocationId, + area->next }; + } + } + + uint64_t resident = 0; + for (uint64_t i = 0; i < pages; i++) + if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, addr + i * 0x1000ULL) != 0) + resident++; + g_heapLocks[slot].Release(); + + Ipc::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages); + Sched::ReleaseUserHeapRange(slot, addr, size); + g_heapLocks[slot].Acquire(); + Sched::g_allocatedPages[slot] -= resident; + g_heapLocks[slot].Release(); + return 0; + } + + inline int64_t Sys_Protect(uint64_t addr, uint64_t size, uint64_t prot) { + auto* proc = Sched::GetCurrentProcessPtr(); + int slot = GetCurrentSlot(); + if (proc == nullptr || slot < 0 || size == 0 || (addr & 0xFFFULL) != 0 || + size > UINT64_MAX - 0xFFFULL || + (prot & ~(VmProtRead | VmProtWrite | VmProtExec)) != 0 || + (prot & VmProtRead) == 0 || + ((prot & VmProtWrite) != 0 && (prot & VmProtExec) != 0)) return -1; + size = (size + 0xFFFULL) & ~0xFFFULL; + if (addr > UINT64_MAX - size) return -1; + + g_heapLocks[slot].Acquire(); + uint64_t pages = size / 0x1000ULL; + + // Protection changes may span adjacent VMA fragments created by a + // previous mprotect. Validate the complete range before changing any + // metadata so failure is atomic from userspace's perspective. + for (uint64_t page = 0; page < pages; page++) { + uint64_t pageVa = addr + page * 0x1000ULL; + bool covered = false; + for (HeapAlloc* area = g_heapAllocs[slot]; area != nullptr; + area = area->next) { + uint64_t areaSize = area->numPages * 0x1000ULL; + if (pageVa >= area->va && pageVa - area->va < areaSize) { + covered = true; + break; + } + } + if (!covered) { g_heapLocks[slot].Release(); return -1; } + } + + uint64_t end = addr + size; + for (HeapAlloc* area = g_heapAllocs[slot]; area != nullptr;) { + HeapAlloc* oldNext = area->next; + uint64_t areaEnd = area->va + area->numPages * 0x1000ULL; + uint64_t overlapStart = area->va > addr ? area->va : addr; + uint64_t overlapEnd = areaEnd < end ? areaEnd : end; + if (overlapStart < overlapEnd) { + uint64_t oldVa = area->va; + uint64_t oldProt = area->prot; + uint64_t prefixPages = (overlapStart - oldVa) / 0x1000ULL; + uint64_t protectedPages = (overlapEnd - overlapStart) / 0x1000ULL; + uint64_t tailPages = (areaEnd - overlapEnd) / 0x1000ULL; + + if (prefixPages == 0) { + area->numPages = protectedPages; + area->prot = prot; + if (tailPages != 0) + area->next = new HeapAlloc { overlapEnd, tailPages, + oldProt, + area->allocationId, + oldNext }; + } else { + area->numPages = prefixPages; + auto* middle = new HeapAlloc { overlapStart, protectedPages, + prot, area->allocationId, + oldNext }; + area->next = middle; + if (tailPages != 0) + middle->next = new HeapAlloc { overlapEnd, tailPages, + oldProt, + area->allocationId, + oldNext }; + } + } + area = oldNext; + } + + bool writable = (prot & VmProtWrite) != 0; + bool executable = (prot & VmProtExec) != 0; + for (uint64_t i = 0; i < pages; i++) { + if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, + addr + i * 0x1000ULL) == 0) + continue; + if (!Memory::VMM::Paging::ProtectUserIn(proc->pml4Phys, + addr + i * 0x1000ULL, + writable, executable)) { + g_heapLocks[slot].Release(); + return -1; + } + } + g_heapLocks[slot].Release(); + Ipc::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages); + return 0; + } + + // Resolve a non-present access inside an anonymous VMA. This is used for + // both ring-3 faults and kernel copies to untouched userspace buffers. + inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, uint64_t errorCode) { + auto* proc = Sched::GetCurrentProcessPtr(); + int slot = GetCurrentSlot(); + if (proc == nullptr || slot < 0 || faultAddr < Sched::UserHeapBase || + faultAddr >= Sched::UserHeapLimit || (errorCode & 1) != 0) return false; + + uint64_t pageVa = faultAddr & ~0xFFFULL; + g_heapLocks[slot].Acquire(); + HeapAlloc* found = nullptr; + for (HeapAlloc* area = g_heapAllocs[slot]; area != nullptr; area = area->next) { + uint64_t bytes = area->numPages * 0x1000ULL; + if (pageVa >= area->va && pageVa - area->va < bytes) { + found = area; + break; + } + } + if (found == nullptr || + ((errorCode & (1ULL << 1)) != 0 && (found->prot & VmProtWrite) == 0) || + ((errorCode & (1ULL << 4)) != 0 && (found->prot & VmProtExec) == 0)) { + g_heapLocks[slot].Release(); + return false; + } + + // A sibling may have resolved the same fault while this CPU waited. + if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, pageVa) != 0) { + g_heapLocks[slot].Release(); + return true; + } + + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) { + g_heapLocks[slot].Release(); + return false; + } + uint64_t phys = Memory::SubHHDM((uint64_t)page); + bool mapped = Memory::VMM::Paging::MapUserInPermissions( + proc->pml4Phys, phys, pageVa, + (found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0); + if (!mapped) Memory::g_pfa->Free(page); + else Sched::g_allocatedPages[slot]++; + g_heapLocks[slot].Release(); + return mapped; + } }; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 129535a..c76a6da 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -118,6 +118,12 @@ namespace montauk::abi { case SYS_FREE: Sys_Free(frame->arg1); return 0; + case SYS_MMAP_ANON: + return (int64_t)Sys_MapAnonymous(frame->arg1, frame->arg2); + case SYS_MUNMAP: + return Sys_Unmap(frame->arg1, frame->arg2); + case SYS_MPROTECT: + return Sys_Protect(frame->arg1, frame->arg2, frame->arg3); case SYS_GETTICKS: return (int64_t)Sys_GetTicks(); case SYS_GETMILLISECONDS: @@ -590,9 +596,9 @@ namespace montauk::abi { // ---- SYSCALL MSR initialization ---- void InitializeSyscalls() { - // Enable SYSCALL/SYSRET in EFER + // Enable SYSCALL/SYSRET and no-execute page permissions in EFER. uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER); - efer |= 1; // SCE bit (Syscall Enable) + efer |= 1 | (1ULL << 11); // SCE | NXE Hal::WriteMSR(Hal::IA32_EFER, efer); // STAR: kernel CS in [47:32], sysret base in [63:48] @@ -608,7 +614,7 @@ namespace montauk::abi { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" - << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 154 syscall slots)"; + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 171 syscall slots)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 6750498..1f58638 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -312,6 +312,9 @@ namespace montauk::abi { // Set path access/modification times. (path, atime, mtime, useCurrent) 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 diff --git a/kernel/src/Api/UserMemory.hpp b/kernel/src/Api/UserMemory.hpp index f995f77..8c67b32 100644 --- a/kernel/src/Api/UserMemory.hpp +++ b/kernel/src/Api/UserMemory.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace montauk::abi::UserMemory { @@ -32,6 +33,21 @@ namespace montauk::abi::UserMemory { uint64_t pml4Phys = CurrentPml4(); if (pml4Phys == 0) return false; + if (Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite)) + return true; + + // Anonymous VMAs are demand-committed. Validation precedes the actual + // kernel copy, so materialize missing pages here with the same access + // type that the copy will perform. + uint64_t page = addr & ~0xFFFULL; + uint64_t last = (addr + size - 1) & ~0xFFFULL; + for (;;) { + if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, page, 1, requireWrite) && + !TryHandleAnonymousPageFault(page, requireWrite ? 2ULL : 0ULL)) + return false; + if (page == last) break; + page += 0x1000ULL; + } return Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite); } @@ -58,7 +74,7 @@ namespace montauk::abi::UserMemory { const char* str = (const char*)addr; for (uint64_t i = 0; i < maxLen; i++) { if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr + i, 1, false)) { - return false; + if (!TryHandleAnonymousPageFault(addr + i, 0)) return false; } if (str[i] == '\0') return true; } diff --git a/kernel/src/Hal/IDT.cpp b/kernel/src/Hal/IDT.cpp index 9a31d64..496779e 100644 --- a/kernel/src/Hal/IDT.cpp +++ b/kernel/src/Hal/IDT.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -201,10 +202,12 @@ namespace Hal { // pushes past the mapped stack and kernel accesses to not-yet-grown // user stack buffers passed into syscalls. if ((errorCode & 1) == 0 && cpu != nullptr && cpu->currentSlot >= 0 - && Sched::GetCurrentPid() >= 0 - && Sched::TryGrowUserStack(cr2)) { - if (fromUser) asm volatile("swapgs"); - return; + && Sched::GetCurrentPid() >= 0) { + if (montauk::abi::TryHandleAnonymousPageFault(cr2, errorCode) || + Sched::TryGrowUserStack(cr2)) { + if (fromUser) asm volatile("swapgs"); + return; + } } // Not a growable fault. Hand the RAW frame (error code at offset 0) diff --git a/kernel/src/Hal/SmpBoot.cpp b/kernel/src/Hal/SmpBoot.cpp index 910faab..a867641 100644 --- a/kernel/src/Hal/SmpBoot.cpp +++ b/kernel/src/Hal/SmpBoot.cpp @@ -188,7 +188,7 @@ namespace Smp { // --- Program SYSCALL MSRs --- uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER); - efer |= 1; // SCE + efer |= 1 | (1ULL << 11); // SCE | NXE Hal::WriteMSR(Hal::IA32_EFER, efer); uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32); diff --git a/kernel/src/Memory/Paging.cpp b/kernel/src/Memory/Paging.cpp index d09d2c4..980bfc6 100644 --- a/kernel/src/Memory/Paging.cpp +++ b/kernel/src/Memory/Paging.cpp @@ -13,13 +13,21 @@ namespace Memory::VMM { static constexpr uint64_t LeafUser = 1ULL << 2; static constexpr uint64_t LeafWriteThrough = 1ULL << 3; static constexpr uint64_t LeafCacheDisabled = 1ULL << 4; + static constexpr uint64_t LeafNoExecute = 1ULL << 63; + // PageTableEntry's historical 52-bit Address bitfield also spans the + // architectural high flag bits. Mask those bits whenever extracting a + // physical address, in particular now that anonymous leaves use NX. + static constexpr uint64_t kPhysAddrMask = 0xFFFFFFFFFFULL; // PTE bits 12..51 static inline void SetLeafPte(PageTableEntry* entry, uint64_t physicalAddress, - bool user, bool writeThrough, bool cacheDisabled) { - uint64_t flags = LeafPresent | LeafWritable; + bool user, bool writeThrough, bool cacheDisabled, + bool writable = true, bool executable = true) { + uint64_t flags = LeafPresent; + if (writable) flags |= LeafWritable; if (user) flags |= LeafUser; if (writeThrough) flags |= LeafWriteThrough; if (cacheDisabled) flags |= LeafCacheDisabled; + if (!executable) flags |= LeafNoExecute; // Replace the complete PTE in one aligned store. Updating individual // bitfields left old PWT/PCD/PAT/accessed state behind when a virtual @@ -230,6 +238,12 @@ namespace Memory::VMM { } bool Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + return MapUserInPermissions(pml4Phys, physicalAddress, virtualAddress, true, true); + } + + bool Paging::MapUserInPermissions(std::uint64_t pml4Phys, std::uint64_t physicalAddress, + std::uint64_t virtualAddress, bool writable, + bool executable) { pagingLock.Acquire(); if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { pagingLock.Release(); @@ -266,7 +280,43 @@ namespace Memory::VMM { if (!pml1) { pagingLock.Release(); return false; } PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); - SetLeafPte(pageEntry, physicalAddress, true, false, false); + SetLeafPte(pageEntry, physicalAddress, true, false, false, writable, executable); + pagingLock.Release(); + return true; + } + + bool Paging::ProtectUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress, + bool writable, bool executable) { + if ((virtualAddress & 0xFFFULL) != 0) return false; + pagingLock.Acquire(); + VirtualAddress va(virtualAddress); + + auto walkRead = [](PageTable* table, uint64_t index) -> PageTable* { + PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]); + if (!entry->Present || !entry->Supervisor || entry->LargerPages) return nullptr; + return (PageTable*)(entry->Address << 12); + }; + + PageTable* pml4 = (PageTable*)pml4Phys; + auto pml3 = walkRead(pml4, va.GetL4Index()); + if (!pml3) { pagingLock.Release(); return false; } + auto pml2 = walkRead(pml3, va.GetL3Index()); + if (!pml2) { pagingLock.Release(); return false; } + auto pml1 = walkRead(pml2, va.GetL2Index()); + if (!pml1) { pagingLock.Release(); return false; } + + PageTableEntry* pte = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); + uint64_t raw = *(volatile uint64_t*)pte; + if ((raw & LeafPresent) == 0 || (raw & LeafUser) == 0) { + pagingLock.Release(); + return false; + } + if (writable) raw |= LeafWritable; + else raw &= ~LeafWritable; + if (executable) raw &= ~LeafNoExecute; + else raw |= LeafNoExecute; + *(volatile uint64_t*)pte = raw; + asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory"); pagingLock.Release(); return true; } @@ -376,7 +426,8 @@ namespace Memory::VMM { // Skip MMIO/WC pages (not PFA-managed) if (pte->WriteThrough || pte->CacheDisabled) continue; - uint64_t pagePhys = (uint64_t)pte->Address << 12; + uint64_t pagePhys = + (uint64_t)(pte->Address & kPhysAddrMask) << 12; if (pagePhys != 0) { Memory::g_pfa->Free((void*)Memory::HHDM(pagePhys)); } @@ -442,11 +493,6 @@ namespace Memory::VMM { return true; } - // Mask to extract only the 40-bit physical address from a PTE Address field - // (bits 12-51 of the PTE). The PageTableEntry::Address field is 52 bits wide - // and includes NX, PK, and software-available bits that must be stripped. - static constexpr uint64_t kPhysAddrMask = 0xFFFFFFFFFFULL; // 40 bits - std::uint64_t Paging::GetPhysAddr(std::uint64_t pml4, std::uint64_t virtualAddress, bool use40BitL1) { VirtualAddress virtualAddressObj(virtualAddress); diff --git a/kernel/src/Memory/Paging.hpp b/kernel/src/Memory/Paging.hpp index 0582a36..766c9cb 100644 --- a/kernel/src/Memory/Paging.hpp +++ b/kernel/src/Memory/Paging.hpp @@ -104,6 +104,17 @@ public: // Map a page into an arbitrary PML4 (specified by physical address) with User bit set. static bool MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); + // Map ordinary user memory with explicit leaf permissions. Read access + // is implied by x86 paging; writable and executable are independent. + static bool MapUserInPermissions(std::uint64_t pml4Phys, + std::uint64_t physicalAddress, + std::uint64_t virtualAddress, + bool writable, bool executable); + + // Change leaf permissions on an existing user page. + static bool ProtectUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress, + bool writable, bool executable); + // Map a page into an arbitrary PML4 with User + Write-Combining attributes. static bool MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 7248e94..4fbf76f 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -493,6 +493,8 @@ namespace Sched { proc.kernelStackTop = kernelStackTop; proc.userStackTop = UserStackTop - 8; proc.heapNext = UserHeapBase; + for (uint64_t i = 0; i < UserHeapBitmapWords; i++) + g_userHeapPageMap[slot][i] = 0; proc.fsBase = tls.fsBase; proc.tlsTemplateVaddr = tls.templateVaddr; proc.tlsFileSize = tls.fileSize; @@ -651,6 +653,7 @@ namespace Sched { } if (!ok) { Ipc::UnmapAndFreeUserRange(sharedPml4, base, mappedPages); + ReleaseUserHeapRange(primarySlot_, base, numPages * 0x1000ULL); Kt::KernelLogStream(Kt::ERROR, "Sched") << "Thread TLS allocation failed"; return -1; @@ -675,8 +678,11 @@ namespace Sched { // Allocate kernel stack. void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages); if (stackMem == nullptr) { - if (threadTlsPages != 0) + if (threadTlsPages != 0) { Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages); + ReleaseUserHeapRange(primarySlot_, threadTlsBase, + threadTlsPages * 0x1000ULL); + } return -1; } memset(stackMem, 0, StackSize); @@ -698,8 +704,11 @@ namespace Sched { if (slot < 0) { schedLock.Release(); Memory::g_pfa->Free(stackMem, StackPages); - if (threadTlsPages != 0) + if (threadTlsPages != 0) { Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages); + ReleaseUserHeapRange(primarySlot_, threadTlsBase, + threadTlsPages * 0x1000ULL); + } return -1; } @@ -774,20 +783,54 @@ namespace Sched { schedLock.Acquire(); Process& primary = processTable[primarySlot_]; - uint64_t base = primary.heapNext; bool validOwner = primary.primarySlot == primarySlot_ && primary.state != ProcessState::Free && primary.state != ProcessState::Terminated; - bool fits = validOwner && base >= UserHeapBase && - base <= UserHeapLimit && size <= UserHeapLimit - base; - if (fits) { - primary.heapNext = base + size; - outVa = base; + bool fits = false; + uint64_t pages = size / 0x1000ULL; + uint64_t runStart = 0; + uint64_t runLength = 0; + + if (validOwner && pages <= UserHeapPages) { + for (uint64_t page = 0; page < UserHeapPages; page++) { + bool used = (g_userHeapPageMap[primarySlot_][page / 64] + & (1ULL << (page % 64))) != 0; + if (used) { + runLength = 0; + continue; + } + if (runLength == 0) runStart = page; + runLength++; + if (runLength == pages) { + for (uint64_t p = runStart; p < runStart + pages; p++) + g_userHeapPageMap[primarySlot_][p / 64] |= 1ULL << (p % 64); + outVa = UserHeapBase + runStart * 0x1000ULL; + uint64_t end = outVa + size; + if (end > primary.heapNext) primary.heapNext = end; + fits = true; + break; + } + } } schedLock.Release(); return fits; } + void ReleaseUserHeapRange(int primarySlot_, uint64_t va, uint64_t size) { + if (primarySlot_ < 0 || primarySlot_ >= MaxProcesses || size == 0 || + (va & 0xFFFULL) != 0 || (size & 0xFFFULL) != 0 || + va < UserHeapBase || va > UserHeapLimit || size > UserHeapLimit - va) { + return; + } + + uint64_t first = (va - UserHeapBase) / 0x1000ULL; + uint64_t pages = size / 0x1000ULL; + schedLock.Acquire(); + for (uint64_t p = first; p < first + pages; p++) + g_userHeapPageMap[primarySlot_][p / 64] &= ~(1ULL << (p % 64)); + schedLock.Release(); + } + static void FreeSiblingThreadTls(Process& thr) { int primarySlot_ = thr.primarySlot; if (primarySlot_ < 0 || primarySlot_ >= MaxProcesses || @@ -811,6 +854,7 @@ namespace Sched { uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000; thr.fsBase = 0; Ipc::UnmapAndFreeUserRange(primary.pml4Phys, base, pages); + ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL); } // Switch away from a slot we have just marked non-runnable while holding diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 44b83ed..6d3add1 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -33,6 +33,9 @@ namespace Sched { // failed bound check is recoverable (SYS_ALLOC returns null); allowing the // heap to cross this boundary would silently replace library PTEs. static constexpr uint64_t UserHeapLimit = 0x60000000ULL; + static constexpr uint64_t UserHeapPages = + (UserHeapLimit - UserHeapBase) / 0x1000ULL; + static constexpr uint64_t UserHeapBitmapWords = (UserHeapPages + 63) / 64; // Surface mappings use reusable, fixed-size per-process slots instead of // consuming heap VA forever. 64 * 32 MiB occupies [0x7000000000, // 0x7080000000), comfortably below the main user stack. @@ -191,6 +194,7 @@ namespace Sched { // reservation is serialized across sibling threads and bounded below the // fixed shared-library mappings. bool ReserveUserHeapRange(int primarySlot, uint64_t size, uint64_t& outVa); + void ReleaseUserHeapRange(int primarySlot, uint64_t va, uint64_t size); // Terminate the currently executing thread. If this is the main thread, // the entire process exits (equivalent to ExitProcess). @@ -258,5 +262,8 @@ namespace Sched { // Per-process allocated page count (tracked by Heap syscalls, separate from Process struct) inline uint64_t g_allocatedPages[MaxProcesses] = {}; + // One bit per page in the bounded userspace heap. Unlike heapNext, this + // makes virtual ranges reusable after unmap and failed reservations. + inline uint64_t g_userHeapPageMap[MaxProcesses][UserHeapBitmapWords] = {}; } diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index e2f0de3..f42c81f 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -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 diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index fd4d607..777f13c 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -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 diff --git a/programs/include/libc/sys/mman.h b/programs/include/libc/sys/mman.h index b5aa231..9c589fe 100644 --- a/programs/include/libc/sys/mman.h +++ b/programs/include/libc/sys/mman.h @@ -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 diff --git a/programs/include/montauk/heap.h b/programs/include/montauk/heap.h index 5ed05ce..dd8cccb 100644 --- a/programs/include/montauk/heap.h +++ b/programs/include/montauk/heap.h @@ -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 -#include +#include +#include + +// The allocator lives in libc. Keeping these declarations here lets +// freestanding C++ programs use the Montauk API without pulling in all of +// , 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 diff --git a/programs/include/montauk/thread.h b/programs/include/montauk/thread.h index 57b3777..d45085a 100644 --- a/programs/include/montauk/thread.h +++ b/programs/include/montauk/thread.h @@ -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). diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index b6344fa..03e02f7 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -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) { diff --git a/programs/lib/libc/obj/libc.o b/programs/lib/libc/obj/libc.o index 14b8f35..dceae4e 100644 Binary files a/programs/lib/libc/obj/libc.o and b/programs/lib/libc/obj/libc.o differ diff --git a/programs/man/malloc.3 b/programs/man/malloc.3 index f1b3f63..cd44718 100644 --- a/programs/man/malloc.3 +++ b/programs/man/malloc.3 @@ -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 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) diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index 5d4a3a8..e24d26b 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -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). diff --git a/programs/src/memtest/main.cpp b/programs/src/memtest/main.cpp new file mode 100644 index 0000000..89a0f57 --- /dev/null +++ b/programs/src/memtest/main.cpp @@ -0,0 +1,171 @@ +/* + * memtest - userspace heap and virtual-memory regression tests + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include +#include + +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); +}