The Physical Frame Allocator
July 16, 2026 ยท View on GitHub
Physical memory is managed one 4 KiB frame at a time by a bitmap allocator, one
bit per frame, seeded at boot from the firmware memory map. It is the bottom of
the memory subsystem: the paging manager, the kernel heap, the page allocator,
and the DMA pools all draw their physical frames from here. This page documents
its state, how it is seeded, how a frame is allocated and freed, the allocation
flags, and the safety checks the free path enforces. The code is under
src/memory/phys/.
The state and the bitmap
The allocator's whole state is one structure
(src/memory/phys/types/allocator_state.rs:18):
AllocatorState
frame_start u64 base physical address of the managed range
frame_count usize number of frames it manages
bitmap_ptr *mut u8 the bitmap, one bit per frame
bitmap_bytes usize size of the bitmap in bytes
next_hint u64 where the next-fit search starts
random_seed u64 seed for placement randomisation
The bitmap is the allocator. Each frame is one bit: a set bit means the frame is
allocated, a clear bit means it is free. Frame i covers the physical range
frame_start + i * 4096. is_initialized (allocator_state.rs:39) is true once
frame_count is non-zero and the bitmap pointer is not null, and every operation
checks it first, so the allocator refuses to hand out or free frames before it is
seeded. The structure is marked Send and Sync because it lives behind a lock
at the module boundary; the functions documented below take it by mutable
reference.
Seeding at boot
The allocator is seeded from the boot handoff during early memory init
(src/kernel_core/init/memory.rs:21):
init_memory(handoff):
pick the single largest usable region from handoff.mmap
if that region is smaller than 1 MiB or invalid:
fall back to (0x100000, 0x8000_0000)
if its start is below 1 MiB, clamp the start up to 0x100000
phys::init(start, end)
on error, or if still not initialised, init_fallback()
once initialised, bring up the DMA display pool
It scans handoff.mmap.usable_regions() and keeps the widest one, so the
allocator is seeded from the largest contiguous block of usable RAM the firmware
reported. The low megabyte is always excluded by clamping the start up to
0x100000, since that region holds legacy structures the kernel does not
allocate over. If the memory map is missing or unusably small, init_fallback
(init/memory.rs:56) tries three hard-coded ranges in turn, and a failure to
initialise at all is logged as CRITICAL. Only once the allocator is live does
memory init bring up the DMA pool that depends on it.
Initialising the bitmap
phys::init reduces to init_with_bitmap (src/memory/phys/allocator/init.rs:23),
which validates the range and the bitmap before it will accept them:
init_with_bitmap(state, managed_start, managed_end, bitmap_ptr, bitmap_bytes):
if managed_end <= managed_start -> InvalidRange
aligned_start = align_up(managed_start, 4096)
aligned_end = align_down(managed_end, 4096)
if aligned_end <= aligned_start -> NoCompletePagesInRange
frame_count = frames_in_range(aligned_start, aligned_end)
required_bytes = bitmap_bytes_for_frames(frame_count)
if bitmap_bytes < required_bytes -> BitmapTooSmall
if bitmap_ptr is null -> InvalidBitmapPointer
store the fields, next_hint = 0, random_seed = derive_seed()
zero the bitmap over required_bytes
The range is aligned inward to whole pages, so a partial page at either end is dropped rather than half-managed. The bitmap is required to be large enough for the whole frame count before it is accepted, and it is zeroed on init, which means every managed frame starts free. The next-fit hint starts at zero and the placement seed is drawn once here.
Allocating a frame
allocate_frame (src/memory/phys/allocator/alloc.rs:20) returns the next free
frame, honouring the requested flags:
allocate_frame(state, flags):
if not initialised -> None
if flags has HIGH:
scan i from the top down; take the first free bit
else:
scan from next_hint upward, wrapping, for the first free bit
set the bit, advance next_hint past it
frame = frame_start + i * 4096
if flags has ZERO: zero the frame
return Some(frame)
The default search is next-fit: it begins at next_hint, scans forward wrapping
around the whole bitmap, and on success advances the hint one past the frame it
took. In the common case a free frame is found immediately after the last one,
which keeps allocation close to constant time, and the wrap guarantees the whole
range is searched before allocation fails. The HIGH flag reverses the search to
run from the top of the range downward, used where a caller wants frames placed
high, and the ZERO flag zero-fills the frame before returning it. If no free bit
exists anywhere, allocation returns None; the allocator never faults or halts on
exhaustion, it reports it, and the caller decides what to do.
Freeing a frame
deallocate_frame (alloc.rs:55) validates a frame thoroughly before clearing
its bit, and in particular detects a double free:
deallocate_frame(state, frame):
if not initialised -> NotInitialized
if frame.addr < frame_start -> AddressBelowRange
offset = frame.addr - frame_start
if offset not page-aligned -> AddressNotAligned
idx = offset / 4096
if idx >= frame_count -> AddressAboveRange
if the bit is already clear -> DoubleFree
clear the bit
A frame outside the managed range, below or above it, is rejected rather than
corrupting the bitmap, an unaligned address is rejected, and an attempt to free a
frame that is already free returns DoubleFree rather than silently clearing an
already-clear bit. The free path cannot be used to mark a frame free twice or to
touch a bit outside the allocator's own range.
Flags and errors
Allocation flags are AllocFlags (src/memory/phys/types/flags.rs): the paths
above use HIGH to place from the top of the range and ZERO to zero-fill a
frame before returning it. The error type is PhysAllocError
(src/memory/phys/error/types.rs); the variants the init, alloc, and free paths
return are InvalidRange, NoCompletePagesInRange, BitmapTooSmall,
InvalidBitmapPointer, NotInitialized, AddressBelowRange, AddressNotAligned,
AddressAboveRange, and DoubleFree.
Security analysis
This allocator is the root of physical memory, so a corruption here is a corruption everywhere above it. Its safety is the discipline of the free path and the refusal to operate before it is seeded. Three properties carry that.
A frame is validated against the managed range before its bit is touched. deallocate_frame
(alloc.rs:55) rejects an address below frame_start (AddressBelowRange), an unaligned address
(AddressNotAligned), and an index at or past frame_count (AddressAboveRange) before it computes
a bit position. So the free path cannot be used to clear a bit outside the allocator's own bitmap,
which means a bad or attacker-influenced address cannot corrupt the free/allocated state of memory
the allocator does not manage.
Double free is a hard error. If the target bit is already clear, deallocate_frame returns
DoubleFree (alloc.rs:72) rather than silently clearing an already-clear bit. This is the bottom
line under the higher-level lifetime tracking: even if a caller loses track of a frame's state, the
bitmap itself refuses to mark the same frame free twice, which is what prevents the classic double-free
that hands the same physical frame to two owners.
It never operates uninitialised, and never faults on exhaustion. Every operation checks
is_initialized first (allocator_state.rs:39), so the allocator refuses to hand out or free frames
before it is seeded from the firmware map, and a genuinely out-of-frames allocate_frame returns
None (alloc.rs:20) rather than faulting or halting: exhaustion is reported and the caller decides.
The low megabyte is excluded at seed time by clamping the start up to 0x100000, so the allocator
never hands out the legacy region. The honest boundary worth stating: this layer does not zero a
frame on allocation unless the ZERO flag is passed, so the no-stale-data guarantee for reused memory
comes from the free path zeroing (the zeroization zero_frame on deallocate) and
from the callers that pass ZERO, not from allocation zeroing by default here.
Debugging the frame allocator
The frame allocator does not narrate to the console; it returns a PhysAllocError
(error/types.rs), and the variant is the whole diagnosis. Seeding and the free path return distinct
ones:
InvalidRange managed_end <= managed_start a bad seed range
NoCompletePagesInRange after inward alignment, nothing is left range too small or misaligned
BitmapTooSmall the bitmap cannot cover frame_count bitmap sized wrong for the range
InvalidBitmapPointer the bitmap pointer is null
NotInitialized an operation ran before seeding
AddressBelowRange a free below frame_start wrong or foreign address
AddressNotAligned a free of a non-page-aligned address a corrupted or fabricated frame
AddressAboveRange a free at or past frame_count wrong or foreign address
DoubleFree a free of an already-free frame a lifetime bug in the caller
The three Address* variants and DoubleFree are the ones that mean a caller handed the allocator a
bad frame, and they are the tell for a lifetime bug or a stray physical address rather than exhaustion.
Allocation exhaustion, by contrast, is not an error variant at all: allocate_frame returns None, so
a caller that maps that to its own "no memory" error (the heap's FrameAllocationFailed,
the DMA path's NoMemory) is reporting real physical pressure. At boot,
the seeding step (kernel_core/init/memory.rs) logs a CRITICAL if the allocator cannot be
initialised at all after the fallback ranges, which is the one console signal from this layer and means
the firmware memory map was unusable.
What sits above it
This allocator hands out single frames. Contiguous multi-frame allocation is a
separate path (src/memory/phys/allocator/contiguous.rs). Above the frame
allocator, the paging manager consumes frames for page
tables and mappings, the kernel heap is backed by frames mapped at a
fixed virtual base, and the DMA pools reserve frames for device buffers. Each of
those has its own page; this one is the source they all draw from.
Source map
src/memory/phys/types/allocator_state.rs the AllocatorState
src/memory/phys/allocator/init.rs init_with_bitmap
src/memory/phys/allocator/alloc.rs allocate_frame, deallocate_frame, the double-free check
src/memory/phys/bitmap/ the bit operations
src/memory/phys/types/flags.rs AllocFlags
src/memory/phys/error/types.rs PhysAllocError
src/kernel_core/init/memory.rs the boot seeding
Every reference above is verified against those trees. The free-path zeroing that makes reused frames
safe is on the zeroization page, and the consumers that turn a None allocation into
their own error are the heap, the paging manager, and the
DMA path.