The Page Allocator
July 16, 2026 ยท View on GitHub
The frame allocator hands out raw physical frames, and the
paging manager maps them. The page allocator sits above both: it is the
kernel's tracked allocator for whole virtual page ranges, the layer that carves a range of
kernel virtual address space, backs it with frames, zeroes it, and remembers it so it can be
freed and zeroed again. Per-process kernel stacks and similar fixed kernel allocations come
from here. The code is under src/memory/page_allocator/.
What it allocates
A request is a size in bytes; the allocator rounds it up to whole pages and returns a kernel
virtual address for the base of the range. allocate_page (manager/alloc.rs:27) is the
core:
allocate_page(size):
reject if not initialized, size == 0, or size > MAX_ALLOCATION_SIZE (1 GiB)
reject if tracked pages >= MAX_TRACKED_PAGES (100_000)
page_count = ceil(size / PAGE_SIZE)
va = allocate_virtual_pages(page_count) // backed by the buddy allocator
pa = translate(va) // resolve the backing frame
record AllocatedPage { page_id, va, pa, time, size }
write_bytes(va, 0, total_size) // zero the whole range
return va
The virtual range and its frame backing come from the buddy allocator
(crate::memory::buddy_alloc, via manager/mapping.rs:22), which allocates the contiguous
virtual pages and maps them; the page allocator adds tracking, physical-address resolution,
and the zeroing. Every allocation is zeroed before it is returned, so a caller never sees a
previous tenant's bytes. The size is bounded above at one gigabyte and the number of live
tracked allocations at one hundred thousand, so neither a single oversize request nor an
unbounded number of small ones can run the tracking table away.
What it tracks
Each live allocation is an AllocatedPage (types/page.rs:20) kept in a Vec behind the
allocator's mutex:
struct AllocatedPage {
page_id: u64, // monotonic, from INITIAL_PAGE_ID = 1
virtual_addr: VirtAddr,
physical_addr: PhysAddr,
allocation_time: u64, // TSC at allocation
size: usize, // rounded-up byte size
}
The record is what lets the allocator answer get_page_info, is_allocated, and the free
path by virtual address; the monotonic page_id and the TSC timestamp make an allocation
identifiable in a dump. The allocator is a single global, PAGE_ALLOCATOR, a Mutex around
the PageAllocator struct (manager/globals.rs:21), initialized once at unified-VM bring-up.
Freeing
deallocate_page (manager/dealloc.rs:26) reverses the allocation and zeroes on the way
out as well:
deallocate_page(va):
idx = tracked page with virtual_addr == va else PageNotFound
page = remove(idx)
write_bytes(va, 0, page.size) // zero before unmap
free_virtual_pages(va, page.size / PAGE_SIZE) // buddy unmap + frame free
record deallocation
Freeing an address that was never allocated here is PageNotFound rather than a silent
unmap, so the allocator will not tear down a range it did not carve. The range is zeroed
before it is unmapped, which pairs with the zero-on-allocate to give the property that page
memory holds no stale content either when handed out or after being reclaimed; this is the
per-allocation half of the zeroization posture.
Statistics
AllocatorStats (types/stats.rs:19) counts total allocations and deallocations, live page
count, total bytes, and a peak page high-water mark maintained with a compare-exchange loop
so a concurrent allocation cannot lose a peak update. The snapshot is available through
get_stats, get_allocation_count, get_total_bytes_allocated, and get_peak_pages
(manager/api.rs), which makes the kernel's virtual-range usage observable at runtime.
Where it sits
page_allocator tracked virtual-range allocation, zero-on-alloc/free, stats
|
v
buddy_alloc contiguous virtual pages, frame backing, mapping and unmapping
|
v
frame allocator raw physical frames paging manager the mappings
The page allocator is the tracked, zeroing, size-bounded front the rest of the kernel calls
for whole-range allocations; the buddy allocator underneath owns the virtual-range bookkeeping
and the actual map and unmap, over the frame allocator and
paging manager. The general-purpose byte allocator capsules and kernel
code reach through alloc is the separate heap; the page allocator is for
page-granular kernel ranges.
Security analysis
The page allocator carves whole kernel virtual ranges, including per-process kernel stacks, so its job is to hand those out clean and bounded and to refuse to tear down a range it did not create. Three properties hold that.
Zero on both ends. allocate_page writes the whole range to zero before returning it
(alloc.rs), and deallocate_page zeroes it again before it unmaps it (dealloc.rs:26). A caller
therefore never sees a previous tenant's bytes when a range is handed out, and reclaimed memory holds
no readable content when it goes back. This is the page-granular half of the
zeroization posture, and it matters most for kernel stacks, where the previous
occupant's saved registers and locals would otherwise linger.
Bounded above and in count. A single request is capped at MAX_ALLOCATION_SIZE (1 GiB) and the
number of live tracked allocations at MAX_TRACKED_PAGES (100000) (constants.rs), both checked at
the top of allocate_page (alloc.rs:27). So neither one oversize request nor an unbounded stream
of small ones can run the tracking Vec away or drain virtual space through this path; an
over-request is refused with InvalidSize or TooManyPages before anything is allocated.
Free only what was allocated here. deallocate_page looks the address up in its tracked set and
returns PageNotFound if it is not there (dealloc.rs:31) rather than issuing a blind unmap. The
allocator will not tear down a range it did not carve, so a bad or stale virtual address cannot be
used to unmap arbitrary kernel memory through this interface. The honest boundary: the tracking is a
Vec scanned by address, not a guarantee about what lies between two allocations, so this layer
gives lifetime and provenance safety, not guard-page isolation between neighbouring ranges. Guard
pages around stacks are the hardening subsystem's job.
Debugging page allocation
Every failure is a PageAllocError variant (error/types.rs) returned to the caller, so a failed
kernel-range allocation names its cause rather than faulting:
NotInitialized the allocator was used before unified-VM bring-up
InvalidSize size == 0, or size > MAX_ALLOCATION_SIZE (1 GiB)
TooManyPages the request exceeds the buddy allocator, or tracked pages >= 100000
FrameAllocationFailed the buddy layer could not back the virtual range with frames
MappingFailed the pages could not be mapped
OutOfVirtualSpace no contiguous virtual range of that size is free
PageNotFound deallocate_page was given an address this allocator never carved
TranslationFailed the backing frame could not be resolved for the tracking record
The two that read as a bug in the caller rather than resource pressure are PageNotFound and
InvalidSize: PageNotFound on a free means the address is wrong or was already freed (a
double-free or a stale handle), and InvalidSize is a zero or absurd request. OutOfVirtualSpace
and TooManyPages are the ceilings biting, and FrameAllocationFailed under them is genuine
physical exhaustion one layer down in the frame allocator. The runtime view is
AllocatorStats (types/stats.rs): total allocations and deallocations, live page count, total
bytes, and a peak high-water mark maintained with a compare-exchange loop so a concurrent allocation
cannot lose a peak update. A live page count that climbs and never falls is a leak in a caller that
allocates a kernel range and never frees it; the monotonic page_id and TSC timestamp on each
AllocatedPage make the leaking allocation identifiable in a dump.
Where it sits
page_allocator tracked virtual-range allocation, zero-on-alloc/free, stats
|
v
buddy_alloc contiguous virtual pages, frame backing, mapping and unmapping
|
v
frame allocator raw physical frames paging manager the mappings
The page allocator is the tracked, zeroing, size-bounded front the rest of the kernel calls
for whole-range allocations; the buddy allocator underneath owns the virtual-range bookkeeping
and the actual map and unmap, over the frame allocator and
paging manager. The general-purpose byte allocator capsules and kernel
code reach through alloc is the separate heap; the page allocator is for
page-granular kernel ranges, and the guard pages and canaries that protect the stacks it carves
are on the hardening page.
Source map
src/memory/page_allocator/manager/alloc.rs allocate_page, zero-on-alloc
src/memory/page_allocator/manager/dealloc.rs deallocate_page, zero-on-free
src/memory/page_allocator/manager/mapping.rs buddy-allocator backing and translation
src/memory/page_allocator/manager/api.rs the public surface and init
src/memory/page_allocator/types/page.rs AllocatedPage, PageInfo
src/memory/page_allocator/types/stats.rs AllocatorStats and the peak high-water mark
src/memory/page_allocator/error/types.rs PageAllocError
src/memory/page_allocator/constants.rs the size and tracking bounds
Every reference above is verified against those trees. The frames come from the physical frame allocator, the mapping goes through the paging manager, the zeroing is the zeroization posture, and the guard pages around carved stacks are on the hardening page.