The Paging Manager
July 16, 2026 ยท View on GitHub
The paging manager owns the kernel's view of virtual memory. It tracks every
address space, every mapping the kernel has installed, the page-table root that is
active on the CPU, and the allocation of address-space identifiers. It is a single
global behind a lock, and every path that maps or unmaps a page goes through it.
This page documents its state, the permission model it installs, the address-space
record, and the mapping interface with its typed helpers and their exact behaviour.
The code is under src/memory/paging/.
The manager state
The whole tracked state of virtual memory is one structure
(src/memory/paging/manager/core/types.rs:24):
PagingManager
active_page_table Option<PhysAddr> the root last loaded into CR3
active_asid Option<u32> the ASID active on the last
switch, for TLB shootdown scope
mappings BTreeMap<u64, PageMapping> every mapping, keyed by VA
address_spaces BTreeMap<u32, AddressSpace> every address space, by ASID
next_asid u32 next ASID to hand out
initialized bool
next_asid starts at FIRST_USER_ASID, so user address spaces are numbered above
the kernel's reserved KERNEL_ASID. active_asid is None before any process
has been dispatched, when the kernel is still running on the boot page tables with
no user CR3 active; once a process is switched to, it records that process's ASID
so the TLB shootdown wrappers can scope their invalidations to the right address
space. The mappings and address_spaces maps are ordered BTreeMaps, so the
manager can answer range and lookup queries over what it has installed. The whole
structure lives behind a single lock at the module boundary, which makes the
interrupt discipline below necessary.
Page permissions
Every mapping carries a PagePermissions, a u32 bitfield with a named constant
per bit (src/memory/paging/types/permissions/flags.rs:20):
READ WRITE EXECUTE USER GLOBAL
NO_CACHE WRITE_THROUGH DEVICE
COW DEMAND ZERO_FILL SHARED LOCKED
The operations on it are the usual set algebra as const fns: contains,
union, insert, remove, and empty. The one predicate that is a security
invariant rather than a convenience is is_wx_violation (flags.rs:60):
is_wx_violation(self) = self.contains(WRITE) and self.contains(EXECUTE)
A permission set is a write-execute violation exactly when it is both writable and
executable. No page in the system is ever supposed to be both, and this predicate
is the test that enforces it. The enforcement point and the guarantees around it
are documented in full on the hardening page; here it is enough to
know that the permission model can name a W^X violation and that the manager's
callers check for it. The remaining flags describe caching (NO_CACHE,
WRITE_THROUGH, DEVICE), sharing and lifecycle (SHARED, LOCKED, COW), and
lazy population (DEMAND, ZERO_FILL), the last two of which are the
fault handler's concern.
Address spaces
An address space is a small record tying an ASID to a page-table root and a
process (src/memory/paging/types/address_space.rs:22):
AddressSpace
asid u32 the address-space identifier
cr3_value PhysAddr the page-table root for this space
process_id u32 the owning process
creation_time u64
is_kernel (address_space.rs:34) reports whether the ASID is the reserved
KERNEL_ASID. The kernel's own address space is registered during unified-VM init
and shared, mapped identically, into the upper half of every process's space; each
process gets its own AddressSpace with a fresh ASID and its own cr3_value whose
lower half is private. The manager keeps these in address_spaces and consults
them when switching CR3 and when scoping TLB shootdowns.
Mapping a page
The public entry point is map_page (src/memory/paging/manager/api/mapping.rs:24),
and the first thing it does is disable interrupts around the manager lock:
map_page(va, pa, perms):
without_interrupts(||
PAGING_MANAGER.lock().map_page(va, pa, perms, Size4KiB, &PAGING_STATS))
The interrupt discipline is not incidental and the source explains it. The manager
is a spin::Mutex. If a timer interrupt fired on this CPU while the lock is held,
the preemption path in the ISR would call switch_to_process_address_space, which
takes the same lock, and the CPU would deadlock on its own mutex. Disabling
interrupts across the critical section closes that window. Every mapping and
unmapping entry point in this module follows the same pattern. Inside the lock the
manager walks the page-table levels, allocating intermediate tables from the
frame allocator as needed, installs the leaf entry, records
the PageMapping in its mappings map, and updates PAGING_STATS. map_huge_page
is the same with a caller-chosen PageSize.
The typed helpers
Most callers do not build a raw permission set; they call a helper that encodes the
correct permissions and cache mode for a kind of memory, and these helpers are
where the system's memory policy is written down (api/mapping.rs).
map_kernel_page READ | WRITE | GLOBAL
map_user_page(w) READ | USER (+ WRITE if w)
map_device_memory READ | WRITE | NO_CACHE | DEVICE
map_user_mmio USER | READ | WRITE | NO_CACHE | DEVICE
map_user_dma USER | READ | WRITE (write-back cacheable)
Three details in these are worth stating because they are correctness, not style.
None of them set EXECUTE, so device, MMIO, and DMA mappings are non-executable by
construction. The MMIO helpers set NO_CACHE because device registers must not be
cached, while the DMA helper deliberately does not: on x86_64 PCI devices snoop the
cache, so a coherent DMA buffer is write-back cacheable, and marking it uncached or
write-combining would be wrong. And map_user_mmio and map_user_dma roll back on
partial failure: if the n-th page of a range fails to map, the helper unmaps the
n-1 pages it already installed before returning the error, so a failed mapping
never leaves a partial range behind.
The source also records who is allowed to call the user device helpers: the comment
on map_user_mmio states that the caller is the hardware broker and no other path
is permitted to expose physical memory to a capsule, and that the helper does not
itself consult the broker tables, so the broker is responsible for confirming the
physical range belongs to a BAR the calling process claimed. The
hardware broker page covers that check.
Unmapping and TLB shootdown
unmap_page (api/mapping.rs:62) unmaps under the same interrupt discipline,
returns the physical address that was mapped along with the permissions and size,
and records the unmapping in the stats. unmap_range walks 4 KiB pages across a
byte length rounded up and unmaps each, stopping at the first failure and returning
it. The unmap path is also where cross-CPU TLB coherency happens: as the comments
on unmap_user_mmio and unmap_user_dma note, unmap_page emits a per-ASID SMP
TLB shootdown through the manager's shootdown wrappers
(src/memory/paging/manager/shootdown.rs), scoped by the active_asid recorded in
the manager state, so a mapping removed on one CPU is invalidated on the others
that share the address space rather than lingering in their TLBs.
Security analysis
The paging manager is the single choke point through which every mapping in the system is installed, so its properties are the ones the rest of the memory subsystem builds on. Four hold.
W^X by construction. The install path map_page_in_asid rejects a permission set that is both
WRITE and EXECUTE with PagingError::WXViolation before it computes a PTE, so no writable-executable
page is ever installed into any address space. The predicate is_wx_violation (flags.rs:60) is where
that rule is named; the hardening page covers the gate itself. Because it is the same
map_page every helper and every fault handler funnels through, the invariant is total: there is one
place mappings are made, and that place enforces it.
The device helpers are non-executable and correctly cached. map_device_memory, map_user_mmio,
and map_user_dma (api/mapping.rs) never set EXECUTE, so a device register window or a DMA buffer
is not a code page by construction. The MMIO helpers set NO_CACHE because device registers must not
be cached; the DMA helper deliberately does not, because on x86_64 PCI devices snoop the cache and a
coherent DMA buffer is write-back cacheable. Marking a snooped DMA buffer uncached would be a
correctness bug, not extra safety, and the helper gets it right.
Partial mappings roll back. map_user_mmio and map_user_dma unmap the pages they already
installed if a later page in the range fails (api/mapping.rs), so a failed range mapping never
leaves a partial window exposed to a capsule. The same discipline is why the install is the
transaction boundary.
The interrupt discipline prevents self-deadlock. Every map and unmap entry point runs the manager
lock inside without_interrupts (api/mapping.rs:24). The manager is a spin::Mutex, and if a timer
interrupt fired while the lock was held, the preemption path would call
switch_to_process_address_space, which takes the same lock, and the CPU would deadlock on its own
mutex. Disabling interrupts across the critical section closes that window and, on the unmap path, keeps
the per-ASID TLB shootdown consistent with the active_asid it reads. The honest boundary the source
records: map_user_mmio does not itself consult the broker tables, so the hardware broker
is trusted to confirm the physical range belongs to a BAR the calling process claimed. The manager
enforces W^X, NX, caching, and rollback; it does not re-check the broker's authority decision.
Debugging the paging manager
Every mapping and unmapping failure is a PagingError variant (error/types.rs), and the manager
carries a string form for each (error/impls.rs) so a failure reads as a sentence:
NotInitialized "Paging manager not initialized" used before init
NoActivePageTable "No active page table" no CR3 recorded yet
FrameAllocationFailed "Failed to allocate page table frame" no frame for an intermediate table
WXViolation "W^X violation: RW+X not allowed" a writable-executable request
PageNotMapped "Page not mapped" unmap or translate of an absent page
Pml4NotPresent ... "PML4/PDPT/PD/PT entry not present" the walk stopped at a missing level
AlreadyMapped "Page already mapped" a double map at the same VA
UnhandledPageFault "Unhandled page fault" the fault path could not classify it
KernelSpaceViolation "Kernel space violation" a user helper aimed at the kernel half
The two that name a hardening or isolation problem are WXViolation and KernelSpaceViolation: the
first is a caller asking for a W+X page and being refused at the gate (and it also bumps the hardening
wx_violations counter), the second is a user-mapping helper pointed at a kernel-half address. The
Pml4NotPresent family tells you exactly which level of the four-level walk found a missing entry,
which is how a translate failure is localised to a level rather than left as "not mapped". A
FrameAllocationFailed here is subtle: it is not the leaf frame but an intermediate page table the
manager could not allocate, so it is physical exhaustion surfacing during a deep map. PAGING_STATS
records page faults, demand loads, and cow faults, so the ratio of demand loads to ordinary faults is
the runtime signal for a capsule that is faulting more than its eager mappings should require.
Where this connects
The manager is brought up, and the kernel's own mappings established, by the
unified-VM init, which also tears down the bootloader's low-half
identity map once the kernel half is confirmed. The DEMAND, ZERO_FILL, and
COW permission bits are populated lazily by the fault handler. The
W^X invariant named here is enforced and its guarantees stated on the
hardening page. And every intermediate page table this manager
allocates comes from the physical frame allocator.
Source map
src/memory/paging/manager/core/types.rs the PagingManager state
src/memory/paging/types/permissions/flags.rs PagePermissions and is_wx_violation
src/memory/paging/types/address_space.rs the AddressSpace record
src/memory/paging/manager/api/mapping.rs map_page and the typed helpers
src/memory/paging/manager/shootdown.rs the per-ASID TLB shootdown
src/memory/paging/error/types.rs the PagingError variants and their strings
Every reference above is verified against those trees. The W^X gate that this permission model feeds is on the hardening page, the lazy bits are populated by the fault handler, the intermediate tables come from the physical frame allocator, and the broker authority the device helpers trust is on the hardware broker page.