Memory Management
Memory management gives the kernel controlled ownership of physical frames, separates user processes, enforces page permissions, and exposes memory authority only through explicit capabilities.
Related
- Go VirtualMemory Contract
records the reserve/commit/decommit contract that extended the
VirtualMemoryimplementation for Go-style arena allocation. - Userspace Runtime describes the typed
VirtualMemoryClientsurface used by allocator and runtime code. - OOM Handling and Swap and Resource Accounting and Quotas define future memory-pressure and quota policy.
- Resource Governance defines structural versus policy limits, aggregate ownership, and exact reservation/release rules.
- Memory Authority Model defines the future cross-cutting contract for memory authority classes, residency, mapping consistency, pins, DMA boundaries, swap eligibility, and proof obligations.
Current Behavior
The frame allocator builds a bitmap from the Limine memory map, marks all non-usable frames as used, reserves frame zero, and reserves its own bitmap frames. The heap is initialized separately for kernel allocation.
Paging initialization builds a new kernel PML4, remaps kernel sections with section-specific permissions, copies upper-half mappings with NX applied and user access stripped, switches CR3, then enables page-global support. SMEP/SMAP are enabled after those mappings are active.
Each user AddressSpace owns its lower-half page tables and clones the
kernel’s upper-half mappings. Dropping an address space walks the user half and
frees mapped frames, committed anonymous frames retained behind VM_PROT_NONE,
and page-table frames. VirtualMemory lets a process reserve anonymous
address ranges, commit and decommit physical backing, unmap reservations, and
protect committed pages. Anonymous reservations charge the process virtual
reservation ledger. Committed anonymous pages charge
ResourceLedger::frame_grant_pages.
FrameAllocator allocation methods return a MemoryObject result capability,
not a physical address. The normal result payload carries the result-cap index,
and the CQE transfer-result record carries the local cap id plus
MemoryObject interface id. MemoryObject.info exposes page count and size;
MemoryObject.map maps page-aligned object ranges into the caller address
space, MemoryObject.unmap removes those borrowed mappings, and
MemoryObject.protect updates their page-table flags. Held MemoryObject caps
charge the holder’s frame_grant_pages ledger, and final CAP_OP_RELEASE or
process exit frees the owned frames once no borrowed address-space mapping still
holds the backing alive.
The enforced frame-grant and virtual-reservation maxima currently come from
the process ledger/ABI construction path, not from the manifest
memoryCommitLimitBytes or frameGrantLimitPages profile fields. Those fields
remain policy data until profile binding reaches the memory owner and defines
how byte commitment and page grants compose. Current release helpers also use
defensive saturating/no-op behavior in places; the accepted target is a typed
generation-bound reservation whose duplicate, oversized, stale, or missing
release is detected rather than silently absorbed.
Design
The kernel keeps physical allocation host-testable by placing bitmap logic in
capos-lib and wrapping it with kernel HHDM access in kernel/src/mem/frame.rs.
Page-table manipulation stays in the kernel because it is architecture-specific.
ELF loading and VirtualMemory both use page-table flags to preserve W^X:
non-executable data gets NX, writable mappings are explicit, and userspace
pages must be USER_ACCESSIBLE. The CapSet and ring bootstrap pages occupy
reserved virtual pages; VirtualMemory rejects ranges that overlap either one.
User-buffer validation for published processes uses the process AddressSpace
mutex. The kernel checks that user pointers stay below the user address limit,
verifies page-table permissions for the requested read/write access, and copies
through the HHDM mapping while holding the same address-space lock. This keeps
validation and use tied to one stable page-table view. The shared
mem::validate::user_buffer_bounds helper checks arithmetic and user-range
bounds only; there is no separate current-CR3 page-table validator. Spawn-time
TLS initialization uses the same copy primitive while the new address space is
still exclusively owned and has not been published to the scheduler.
Committed VirtualMemory pages and held MemoryObject caps use the same
per-process frame-grant ledger, with quota checks before frame allocation or
mapping side effects. Anonymous reservation consumes a separate virtual page
quota, so guard ranges and Go-style sysReserve arenas do not spend physical
commit budget. Held MemoryObject caps charge for the backing they keep
reachable, and each live borrowed MemoryObject mapping reserves frame-grant
pages until it is unmapped. This prevents a process from mapping an object,
releasing the cap to drop the cap-slot charge, and keeping the backing pinned
without quota. The address space records borrowed pages separately from sparse
anonymous reservations so teardown and unmap can distinguish anonymous pages
from object-backed pages. Future file/network/DMA resources should reuse that
authority ledger instead of adding one-off counters per cap.
Invariants
- Frame addresses are 4 KiB aligned.
- The frame bitmap’s own frames are never returned as free frames.
- Upper-half kernel mappings are not user-accessible.
- Kernel text is RX, rodata is read-only NX, and data/bss are RW NX.
- User address spaces own only lower-half page-table frames.
- Process frame-grant usage covers committed anonymous VM pages, held
MemoryObjectcaps, and live borrowedMemoryObjectmappings. - Process virtual-reservation usage covers reserved anonymous VM pages whether or not they are committed.
- Committed
VM_PROT_NONEpages retain their frames and data while exposing no present user PTE; reserved uncommitted pages consume no frame-grant quota. - Object-backed user mappings are tracked as borrowed pages and hold the
MemoryObjectbacking alive until unmapped or address-space teardown. MemoryObjectunmap/protect only succeeds for borrowed pages backed by the same object.VirtualMemorycaps are bound to one address space and are not valid cross-process service exports.- CapSet is read-only/no-execute; ring is writable/no-execute.
VirtualMemorycannot reserve, map, commit, decommit, unmap, or protect the ring or CapSet pages.VirtualMemorycommit/decommit/protect/unmap only succeeds for ranges covered by anonymous reservations owned by the cap’s address space.- Capability-ring CALL/RECV/RETURN buffers, transfer descriptors, process and
thread wait completions, and private ParkSpace word reads must validate and
copy/read while holding the target process
AddressSpacelock.
OOM Boundary Normalization
The untrusted-reachable memory-authority capabilities distinguish four typed
allocation-failure classes. Every class is a transient “temporary lack of
resources” condition in the Cap’n Proto sense – releasing
capabilities/reservations/frames or retrying later can succeed – so all four
serialize as overloaded (never failed). They are told apart by a stable,
machine-greppable token appended in brackets to the exception message.
capos_lib::mem_failure (MemoryExhaustion) is the single source of truth for
the class-to-token mapping; the kernel constructs the error through
cap::memory_exhaustion_error, which routes it to overloaded and appends the
token.
| Class | Token | Origin | Emitting boundary |
|---|---|---|---|
| Virtual quota | virtual-quota-exhausted | per-process virtual reservation budget (ResourceLedger::reserve_virtual_reservation_pages) | VirtualMemory.reserve |
| Physical commit | physical-commit-exhausted | per-process frame-grant budget (ResourceLedger::reserve_frame_grant_pages) | VirtualMemory.commit, FrameAllocator.allocFrame/allocContiguous, MemoryObject.map |
| Global frame pressure | global-frame-pressure | system-wide frame allocator empty within quota: page backing (frame::alloc_frame_zeroed/alloc_contiguous -> None) or an intermediate page-table frame during map_to (MapToError::FrameAllocationFailed) | VirtualMemory.commit, VirtualMemory.protect, FrameAllocator.allocFrame/allocContiguous, MemoryObject.map, ProcessSpawner.spawn image loading |
| Result-cap publication | result-cap-publication-failed | caller cap-slot budget, cap-table full, or result-cap backing allocation (reserve_cap_slot_insertions, Arc::try_new, insert_with_hold) | FrameAllocator.allocFrame/allocContiguous |
Before this normalization the same failure produced different wire types across
files: the per-process frame-grant quota was failed in
kernel/src/cap/virtual_memory.rs but overloaded in
kernel/src/cap/frame_alloc.rs, and real physical OOM was failed while
cap-table publication OOM was overloaded. The classifier removes that split,
including the page-table-frame OOM inside map_to: the first-mapping paths
(VirtualMemory.commit, MemoryObject.map, and VirtualMemory.protect
transitioning a VM_PROT_NONE-committed page to an accessible protection –
which is that page’s first map_to, since a VM_PROT_NONE commit never maps)
route MapToError::FrameAllocationFailed to global-frame-pressure rather than
collapsing it to failed. The protect-rollback re-map path re-maps an address
whose intermediate page tables already exist, so map_to cannot allocate a new
page-table frame there; it keeps the plain failed string for its genuine
internal-invariant faults.
Kernel-internal bookkeeping allocations (TLB deferred-completion slots,
reservation/commit tracking Vec growth) are not one of these four
caller-attributable classes; they already fail closed as generic overloaded
and are intentionally untokened.
ProcessSpawner types image-load failures before the capability boundary.
Physical-frame exhaustion while creating the address-space root, mapping the
ELF, stack, or TLS, or allocating an intermediate page table routes through
GlobalFramePressure. Fallible kernel bookkeeping allocation during image
construction remains an ordinary, untokened construction overload.
Validation, address arithmetic, mapping, and copy invariant failures remain
failed; they are not mislabeled as retryable memory pressure.
Process/thread limits, child-cap construction, and scheduler run-queue capacity
remain their existing distinct resource family and continue to report
untokened overloaded results.
The DDF hardware-mapping paths (kernel/src/cap/device_mmio.rs,
kernel/src/cap/dma_buffer.rs) also allocate page-table frames through
AddressSpace::map, but those caps are privileged hardware authority granted
only to trusted driver processes, not reachable by an untrusted process, so they
are outside this untrusted-reachable taxonomy and keep their existing failed
mapping errors.
Current State-Transition Inventory
This inventory describes the current implementation, not the future pin, swap, or shared-park design. A deferred TLB completion is part of a transition: frame reuse is not complete until the recorded remote shootdown generation has been acknowledged.
Authority, ledgers, and locks
| Resource or state | Authority and record of truth | Accounting record | Serialization |
|---|---|---|---|
| Physical frame availability | Kernel frame allocation through FrameAllocCap, VirtualMemoryCap, page-table allocation, and process construction; FrameBitmap is the allocation record | Global free-frame count in FrameBitmap; per-process charges are separate | Global mem::frame::ALLOCATOR mutex in alloc_frame, alloc_contiguous, and try_free_frame |
| Anonymous virtual reservation | Address-space-bound VirtualMemoryCap; AddressSpace::anonymous_reservations records intervals | Caller ResourceLedger::virtual_reservation_pages_used | Caller Process::caps mutex during capability dispatch, then the target AddressSpace mutex through address_space_lock |
| Committed anonymous backing | The same VirtualMemoryCap; each AnonymousCommittedPage records frame, protection, and any pending shootdown | Caller ResourceLedger::frame_grant_pages_used | Caller cap-table mutex plus the target AddressSpace mutex; global frame allocator lock is taken per allocation/free |
Held MemoryObject backing | A MemoryObjectCap cap-table slot holds MemoryObjectBacking by Arc | The holder’s cap-slot charge and frame_grant_pages_used, charged by CapTable::insert_with_epoch | Holder Process::caps mutex; backing lifetime is the last Arc, not a parallel counter |
Borrowed MemoryObject mapping | MemoryObjectCap plus caller address space; AddressSpace::borrowed_vm_pages records a backing-owner Arc per page | Mapper frame_grant_pages_used for every live borrowed page | Caller cap-table mutex plus mapper AddressSpace mutex |
| User page-table residency and deferred reuse | AddressSpace owns lower-half tables; the monotonic resident_cpu_mask records every CPU that has loaded the root; ShootdownCompletion records remote generations | No second authority ledger; the bounded DEFERRED_COMPLETIONS queue is completion storage | Address-space mutex for user PTEs; SHOOTDOWN_LOCK orders generations; DEFERRED_COMPLETIONS has its own mutex |
| Private ParkSpace waiter | Process-local ParkSpaceCap; Process::park_waiters and the thread’s BlockReason are the waiter records | One existing thread record plus one reserved_park_cqes credit | Global scheduler lock; range cleanup uses SchedulerLockSite::WakeUnblock |
Anonymous VirtualMemory
| Before | Operation and after | Ledger, completion, and cleanup | Performing symbols |
|---|---|---|---|
| Unreserved | reserve creates a sparse AnonymousReservation; no PTE or frame is installed | Charges virtual-reservation pages before taking the address-space lock. Validation, overlap, hint-selection, or tracking failure releases the charge. Result serialization failure calls the ordinary unmap rollback. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::parse_reserve, VirtualMemoryCap::reserve_region); kernel/src/mem/paging.rs (AddressSpace::reserve_anonymous_vm_range) |
| Unreserved | Compatibility map reserves, serializes the address into kernel reply storage, then commits; success is committed accessible or committed inaccessible | Reserve failure leaves no state. Reply serialization or commit failure calls VirtualMemoryCap::unmap_region; the rollback has the same post-mutation failure limits as ordinary commit and unmap. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::parse_map, VirtualMemoryCap::reserve_region, VirtualMemoryCap::commit_region, VirtualMemoryCap::unmap_region) |
| Reserved and uncommitted | commit allocates zeroed frames and records each page as committed accessible or committed inaccessible (VM_PROT_NONE) | Charges frame-grant pages and reserves deferred-completion slots before mutation. Expected allocation, mapping, or metadata failure invokes rollback; rollback_committed_pages removes the installed PTEs through the infallible unmap_present primitive and schedules each frame for its shootdown-gated release. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::commit_region, VirtualMemoryCap::map_committed_page, VirtualMemoryCap::rollback_committed_pages); kernel/src/mem/paging.rs (AddressSpace::record_anonymous_committed_pages) |
| Committed accessible or inaccessible | protect changes PTE permissions, removes a PTE for VM_PROT_NONE, or restores a retained frame from VM_PROT_NONE | The frame charge is unchanged. On full-range success, all accumulated local-flush completions are deferred. The one reachable failure – the VM_PROT_NONE -> accessible first map_to hitting page-table-frame OOM – replays prior protection and metadata through the now-infallible rollback_protect_changes (its re-map/flag-restore/unmap sub-operations cannot fail), always restoring the coherent before-state. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::parse_protect, VirtualMemoryCap::rollback_protect_changes); kernel/src/mem/paging.rs (AddressSpace::protect_present, AddressSpace::remap_present, AddressSpace::set_anonymous_page_prot) |
| Committed, within a retained reservation | decommit removes committed-page metadata and returns the range to reserved/uncommitted state | On success, present PTEs are locally unmapped, frames are freed only after their current or stored shootdown completion, the frame charge is released, and private ParkSpace waiters are interrupted. The PTE-removal loop uses the infallible unmap_present primitive, so once the range passes preflight the committed-record detach, frame deferral, charge release, and Park cleanup all run to completion; a genuine page-table/metadata divergence fail-stops rather than returning a partial state. The virtual charge remains in either case. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::parse_decommit, VirtualMemoryCap::unmap_present_page); kernel/src/mem/paging.rs (AddressSpace::decommit_anonymous_range); kernel/src/sched.rs (interrupt_park_waiters_for_unmapped_range) |
| Reserved, with zero or more committed pages | unmap removes the requested reservation range, splitting an interval when needed, and leaves it unreserved | On success, present frames are freed only after shootdown, retained VM_PROT_NONE frames use their stored completion, both ledger charges are released, and private ParkSpace waiters are interrupted. The PTE-removal loop uses the infallible unmap_present primitive, so once the range passes preflight the reservation/committed-record changes, frame deferral, ledger release, and Park cleanup all complete; a genuine page-table/metadata divergence fail-stops rather than returning a partial state. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::parse_unmap, VirtualMemoryCap::unmap_region); kernel/src/mem/paging.rs (AddressSpace::release_anonymous_reservation_range, AddressSpace::preflight_middle_split_tail) |
| Live address space targeted externally | External termination removes a process only after it is nonresident, then drops its user mappings and page tables | terminate_process_locked rejects a pid that is current or in handoff on any non-idle CPU. Once it removes the process, finish_terminated_process releases its capabilities and stacks; dropping the returned Process then runs AddressSpace::drop, which frees ordinary owned leaves, committed anonymous frames, and lower-half table frames while borrowed owner Arcs drop with the fields. | kernel/src/sched.rs (non_idle_pid_current_on_any_cpu, terminate_process_locked, finish_terminated_process); kernel/src/process.rs (Process::release_caps_for_exit); kernel/src/mem/paging.rs (AddressSpace::drop) |
| Current address space whose last live thread exits | Self-exit removes the current process, dispatches another context, and defers destruction of the old mappings and page tables | exit_current writes the kernel PML4 before removing the still-current process, then clears current-thread state and releases capabilities. It stores the Process in the per-CPU pending-drop slot so final AddressSpace::drop runs from another stack. | kernel/src/sched.rs (exit_current, defer_current_cpu_pending_drop_locked, drop_pending_process); kernel/src/process.rs (Process::release_caps_for_exit); kernel/src/mem/paging.rs (AddressSpace::drop); kernel/src/arch/x86_64/tlb.rs (write_cr3) |
MemoryObject
| Before | Operation and after | Ledger, completion, and cleanup | Performing symbols |
|---|---|---|---|
| Free frames | FrameAllocator allocates zeroed backing, wraps it in MemoryObjectBacking, and publishes one result cap | Result-record shape, cap-slot capacity, and frame quota are preflighted. Cap insertion atomically charges the cap slot and held-backing pages. | kernel/src/cap/frame_alloc.rs (FrameAllocCap::allocate_memory_object, FrameAllocCap::allocate_range, FrameAllocCap::complete_alloc); capos-lib/src/cap_table.rs (CapTable::insert_with_epoch) |
| Allocated but not successfully published | Allocation, Arc creation, cap insertion, or transfer-result serialization fails; state returns to free frames | Pre-publication failures drop MemoryObjectBacking; a post-insertion serialization failure removes the cap, releasing its ledger charge before the backing drops. MemoryObjectBacking::drop returns every frame through the global allocator. | kernel/src/cap/frame_alloc.rs (FrameAllocCap::complete_alloc, MemoryObjectBacking::drop, free_range); capos-lib/src/cap_table.rs (CapTable::remove) |
| Held backing, unmapped in the caller | MemoryObject.map installs borrowed PTEs and records backing-owner Arcs; state becomes held plus borrowed-mapped | Charges mapper frame-grant pages and reserves TLB completion slots first. Partial-map and tracking errors call unmap_without_forgetting, which removes the just-installed PTEs through the infallible unmap_present primitive, then release the charge. Result-serialization failure calls unmap_region and releases the charge only when rollback succeeds. | kernel/src/cap/frame_alloc.rs (MemoryObjectCap::map, MemoryObjectCap::unmap_without_forgetting, MemoryObjectCap::unmap_region); kernel/src/mem/paging.rs (AddressSpace::record_borrowed_vm_range) |
| Borrowed-mapped | MemoryObject.protect changes permissions while preserving the same backing identity | Mapping and held-cap charges are unchanged. The address-space lock verifies the range belongs to the same backing, which proves every page present, so each PTE update goes through the infallible protect_present primitive and the whole range re-flags with no mixed old/new-permission window; the accumulated shootdown completions are deferred after the range succeeds, and a genuine page-table/metadata divergence fail-stops. | kernel/src/cap/frame_alloc.rs (MemoryObjectCap::protect); kernel/src/mem/paging.rs (AddressSpace::owns_borrowed_vm_range_from, AddressSpace::protect_present) |
| Borrowed-mapped | MemoryObject.unmap removes PTEs and owner records; state returns to held but unmapped for that range | The same-object check runs under the address-space lock. The owner records are forgotten first (guarded by that check, so no leaf is touched if the detach cannot complete), then the infallible unmap_present primitive removes every borrowed leaf before deferred remote completion; the mapping charge is released and private ParkSpace waiters are interrupted. A genuine page-table/metadata divergence fail-stops rather than returning a partially unmapped range. | kernel/src/cap/frame_alloc.rs (MemoryObjectCap::unmap, MemoryObjectCap::unmap_region); kernel/src/mem/paging.rs (AddressSpace::forget_borrowed_vm_range_from); kernel/src/sched.rs (interrupt_park_waiters_for_unmapped_range) |
| Held through one or more cap slots | CAP_OP_RELEASE, transfer rollback, or process cap-table teardown removes a hold | CapTable::remove releases the slot and held-backing frame charge. Frames are freed only when no cap or borrowed-mapping Arc retains MemoryObjectBacking. The immediate ring drain drains deferred TLB completions before running deferred cap releases, so a same-drain final unmap and final cap release drop the backing only after every remote acknowledgement; a cfg(qemu) guard in MemoryObjectBacking::drop flags any regression. | kernel/src/cap/ring.rs (dispatch_release, RingScratch::drain_deferred_releases); kernel/src/arch/x86_64/syscall.rs (cap_enter); kernel/src/sched.rs (service_sqpoll_snapshot); kernel/src/process.rs (Process::release_caps_for_exit); capos-lib/src/cap_table.rs (CapTable::remove, CapTable::clear); kernel/src/cap/frame_alloc.rs (MemoryObjectBacking::drop, prove_final_release_tlb_ordering) |
| Borrowed mapping in a dying address space | AddressSpace::drop removes the user PTE/table tree, then drops per-page backing-owner holds | The generic leaf walk deliberately does not free borrowed frames. Backing is reclaimed only if the field drop removes the final borrowed hold and no cap hold remains. Process removal/non-residency makes a separate teardown shootdown unnecessary in the current no-PCID scheduler. | kernel/src/mem/paging.rs (AddressSpace::drop, AddressSpace::is_borrowed_user_page); kernel/src/sched.rs (terminate_process_locked, exit_current) |
Page-table and TLB completion paths
| Mutation path | Lock and local action | Remote completion and reuse rule | Performing symbols |
|---|---|---|---|
| User 4 KiB map, unmap, or permission change | Caller holds the AddressSpace mutex; map_to, unmap, or update_flags is followed by the local mapper flush | AddressSpace::shootdown_page snapshots the conservative resident-CPU mask and requests a generation from every other online CPU in that mask | kernel/src/mem/paging.rs (AddressSpace::map_with_error, the infallible-after-preflight AddressSpace::unmap_present/protect_present/remap_present for untrusted anon/MemoryObject paths, the fallible AddressSpace::unmap for privileged DDF paths, AddressSpace::shootdown_page); kernel/src/arch/x86_64/tlb.rs (shootdown_page) |
| Address-space residency | Before loading a user CR3, scheduler dispatch paths OR the current CPU into resident_cpu_mask; bits are not cleared, so the mask remains a safe superset | Later user PTE mutations target every online CPU that may retain a translation. CR3 writes use no PCID in the current implementation. | kernel/src/mem/paging.rs (AddressSpace::mark_current_cpu_resident); kernel/src/sched.rs (next_start_context, capos_block_current_syscall, schedule, exit_current, exit_current_thread); kernel/src/arch/x86_64/tlb.rs (write_cr3) |
| Deferred user completion or frame free | Mutation code reserves bounded queue slots before changing PTEs. Successful paths enqueue either a wait or a frame plus completion; the failure-atomic transitions above enqueue every accumulated completion on both their success and their infallible rollback path. | For records that were enqueued, drain_deferred_completions flushes pending local generations, waits for every target acknowledgement, and only then frees a carried frame; the wait() lexically precedes the free, and a cfg(qemu) tripwire in the FreeFrame arm prints ORDERING VIOLATION if that wait() is ever removed/weakened so a frame reaches the allocator before its acknowledgement, with make run-tlb-frame-reuse-ordering the SMP proof exercising a real resident-sibling unmap through that arm. | kernel/src/arch/x86_64/tlb.rs (reserve_deferred_completion_slots, DeferredCompletionReservation::defer_completion, DeferredCompletionReservation::defer_frame_free, drain_deferred_completions); kernel/src/arch/x86_64/syscall.rs (cap_enter); kernel/src/sched.rs (service_sqpoll_snapshot, schedule); kernel/src/cap/frame_alloc.rs (prove_tlb_frame_reuse_ordering) |
| Immediate ring object release | process_ring removes cap-table holds and stores their objects in bounded RingScratch release storage | cap_enter and service_sqpoll_snapshot drain drain_deferred_completions before those release objects, so a final MemoryObject backing drop waits for the earlier same-drain unmap’s remote acknowledgement before returning its frames. A cfg(qemu) guard in MemoryObjectBacking::drop asserts no completion is still pending at free, and the run-memoryobject-final-release-ordering SMP proof exercises the ordering with a real resident sibling. | kernel/src/cap/ring.rs (process_ring, dispatch_release, RingScratch::drain_deferred_releases); kernel/src/arch/x86_64/syscall.rs (cap_enter); kernel/src/sched.rs (service_sqpoll_snapshot); kernel/src/arch/x86_64/tlb.rs (drain_deferred_completions, deferred_completions_pending); kernel/src/cap/frame_alloc.rs (prove_final_release_tlb_ordering) |
| Boot kernel-root replacement and AP trampoline identity mapping | paging::init runs before userspace and replaces the boot root before enabling global pages. AP trampoline mapping holds KERNEL_PAGE_TABLE_LOCK and is boot-only. | The BSP performs local flush/CR3 replacement while no user address space is active; these are not post-userspace user-frame reuse paths. | kernel/src/mem/paging.rs (init, remap_range, map_ap_trampoline_identity, map_identity_page_in_l4) |
| Post-init kernel MMIO or firmware mapping | KERNEL_PAGE_TABLE_LOCK covers top-level checks, intermediate-table walks, and PTE installation; the virtual window is the monotonic NEXT_KERNEL_MMIO_VADDR allocation record | Each page is locally flushed, then kernel_tlb_shootdown_all waits for every other online CPU. Partial install rollback unmaps installed PTEs and performs the same global shootdown; the consumed virtual window and intermediate tables remain reserved. | kernel/src/mem/paging.rs (map_kernel_physical_range, install_kernel_physical_pages, reserve_kernel_mapping_window); kernel/src/arch/x86_64/tlb.rs (kernel_tlb_shootdown_all, flush_all_including_global) |
User-buffer stability paths
| Path | Current stability guarantee | Performing symbols |
|---|---|---|
| Shared bounds and copy primitive | user_buffer_bounds rejects arithmetic or user-range overflow but does not inspect page tables. ValidatedUserRegion borrows the locked AddressSpace, and its copy methods re-walk every page through HHDM while that borrow remains live. | kernel/src/mem/validate.rs (user_buffer_bounds); kernel/src/mem/paging.rs (AddressSpace::validate_user_buffer, AddressSpace::copy_to_user, AddressSpace::copy_from_user, ValidatedUserRegion::copy_in, ValidatedUserRegion::copy_out) |
| Capability-ring parameters and results | The first dispatch_call check is fail-fast only. The authoritative parameter read and result write each reacquire the address-space mutex and revalidate while copying, so no validation result is carried across a lock release. | kernel/src/cap/ring.rs (dispatch_call, validate_current_user_buffer, copy_current_user_params, copy_current_user_result) |
| Capability-transfer descriptors | Descriptor bounds are derived first; one address-space lock covers page validation and the copy into fixed kernel storage before descriptor parsing. | kernel/src/cap/transfer.rs (load_transfer_descriptors); kernel/src/mem/paging.rs (AddressSpace::copy_from_user) |
| Private ParkSpace and measurement-only park reads | The scheduler lock serializes private wait registration with wake; the address-space mutex covers validation and the 32-bit read. The ordinary and measurement paths both use AddressSpace::read_user_u32. | kernel/src/sched.rs (park, read_user_park_word, read_process_user_u32); kernel/src/cap/ring.rs (dispatch_compact_park_bench) |
| Cross-process kernel copies | Scheduler lookup clones the target address-space handle, then the address-space mutex covers the complete revalidating copy/read. | kernel/src/sched.rs (copy_to_process_user, read_process_user_u32) |
| Spawn-time TLS initialization | The address space is not scheduler-visible yet; the loader holds exclusive mutable ownership and uses the same page-walking copy helper. | kernel/src/spawn.rs (load_tls); kernel/src/mem/paging.rs (AddressSpace::copy_to_user) |
ParkSpace cleanup at memory transitions
| Event | Current cleanup | Performing symbols |
|---|---|---|
Anonymous VirtualMemory.unmap or decommit | After a successful PTE/metadata mutation and before the capability call completes, range cleanup removes every matching waiter from the address-keyed table and posts PARK_INTERRUPTED; if the CQE cannot be posted immediately, the exact status remains in BlockReason::ParkCompletion for retry and is not restored under the reusable address. Because the post-preflight PTE removal is infallible, this cleanup always runs once the range passes preflight; a genuine page-table/metadata divergence fail-stops. | kernel/src/cap/virtual_memory.rs (VirtualMemoryCap::unmap_region, VirtualMemoryCap::parse_decommit); kernel/src/sched.rs (interrupt_park_waiters_for_unmapped_range, mark_park_waiter_completion_locked, complete_pending_park_waiters_locked) |
MemoryObject.unmap | The same private-range interruption runs after removal of all borrowed PTEs and owner records and before the call completes. Because the borrowed-PTE removal is infallible after the ownership check, this cleanup always runs; a genuine page-table/metadata divergence fail-stops. | kernel/src/cap/frame_alloc.rs (MemoryObjectCap::unmap); kernel/src/sched.rs (interrupt_park_waiters_for_unmapped_range) |
| Thread exit | The thread’s waiter record is removed and its reserved CQE credit released before the thread record can remain as exited/joinable state. | kernel/src/sched.rs (exit_current_thread, remove_park_waiter_for_thread_locked); kernel/src/process.rs (Process::remove_park_waiter_for_thread) |
| Process exit and address-space drop | Removing the Process drops its fixed waiter table and credits without posting into the dying process. The pid/process generation check makes old private waiters irrelevant before that numeric address can belong to another process. | kernel/src/sched.rs (terminate_process_locked, exit_current); kernel/src/process.rs (Process::park_waiters) |
Final ParkSpaceCap release | The cap is stateless submit authority. Releasing it does not remove process-owned waiters; wake/timeout/unmap/thread-exit/process-exit paths still own their completion or cleanup. | kernel/src/cap/park_space.rs (ParkSpaceCap); kernel/src/cap/ring.rs (dispatch_release); kernel/src/process.rs (Process::park_waiters) |
The inventory records control flow; it is not a substitute for the remaining
proofs. The failure-atomicity residual is now implemented – the post-mutation
sections are infallible-after-preflight or restore a coherent state through an
infallible rollback (see Transition Failure Atomicity).
Exact remaining work is tracked by
final MemoryObject release ordering,
VM ownership host-test residuals,
and the SMP TLB/frame-reuse proof.
The accepted Shared Mapping Identity and Object Pins
contract now defines the object identity, offset, generation, pin, and
validation rules. Shared park keys remain disabled until those records, pins,
and cleanup paths are implemented and proven.
Transition Failure Atomicity
Once a user-memory transition begins mutating page tables it must not return to
userspace with page tables, ownership records, quota ledgers, or
deferred-completion records disagreeing. Each untrusted-reachable transition
(anonymous decommit/unmap/protect, MemoryObject.unmap/protect) is
split into a fallible preflight that mutates nothing and an infallible commit:
- The preflight validates the range, resolves the tracking records, reserves the deferred-completion slots, and reserves any bookkeeping growth. Every failure that can be reached under quota or global pressure is raised here, before any page table or record changes, so a rejection leaves the exact before-state.
- The commit removes or re-flags leaf PTEs through the infallible primitives
AddressSpace::unmap_present,AddressSpace::protect_present, and (for rollback re-maps)AddressSpace::remap_present. Each was proven present (or, for a re-map, proven to still own its intermediate tables) under the held address-space lock, so the underlyingMappercall cannot fail:unmapclears only the leaf entry and leaves L2/L3/L4 intact (so it never frees a frame the metadata still references),update_flagsrewrites an existing leaf, and the rollbackmap_toreuses the intermediate tablesunmapleft behind and allocates nothing. A failure at that point means the hardware page tables have diverged from the tracking metadata under the lock – an unrecoverable invariant break – so the primitive fail-stops rather than hand userspace a partially torn-down range.
protect keeps one genuinely fallible commit step: a VM_PROT_NONE -> accessible
transition performs the page’s first map_to, which can hit page-table-frame
global-frame-pressure. On that failure the applied steps roll back newest
first, and because every rollback sub-operation is one of the infallible
primitives above, the rollback itself cannot fail: the range always returns to
one coherent before-state instead of “returning from a failed rollback”.
Any frame a transition releases is handed to defer_frame_free, which holds it
out of the allocator until the local flush and the remote shootdown completion
for its page have finished (kernel/src/arch/x86_64/tlb.rs,
drain_deferred_completions); borrowed MemoryObject frames are owned by the
object backing and are freed on its Drop, so their unmap defers only the
shootdown completion. VirtualMemory.decommit, VirtualMemory.unmap, and the
commit rollback of already-committed pages all funnel their per-page unmap
completion into defer_frame_free, whose drain_deferred_completions FreeFrame
arm flushes locally, waits for the remote acknowledgement, and only then returns
the frame. The ordering is structural – the wait() lexically precedes the free
– and a cfg(qemu) tripwire in that arm prints an ORDERING VIOLATION line if a
future edit ever removes or weakens that wait() so a frame reaches the allocator
without its remote acknowledgement complete.
make run-tlb-frame-reuse-ordering is the focused SMP proof that drives a real
resident-sibling unmap through that exact machinery and shows same-physical reuse
only after the ack. The immediate ring drain (cap_enter,
service_sqpoll_snapshot) runs drain_deferred_completions before its deferred
capability drops, so a same-drain final unmap and final release trigger that
Drop only after every remote acknowledgement – frames are never returned to
the allocator while a sibling may still hold a stale translation. A cfg(qemu)
guard in MemoryObjectBacking::drop asserts no completion is pending at free,
and make run-memoryobject-final-release-ordering is the focused SMP proof.
Two frame-free paths deliberately do not issue a shootdown, because no sibling CPU can hold a stale translation to the frame:
- Commit rollback of the just-allocated, never-mapped frame. When
map_committed_pagefails, the frame allocated for that page was never installed into a live PTE, soVirtualMemoryCap::commit_regionfrees it directly (kernel/src/cap/virtual_memory.rs). A frame that was never mapped cannot be cached in any TLB. - Process exit (
AddressSpace::drop). External termination removes a process only after it is nonresident (terminate_process_lockedrejects a pid that is current or in handoff on any non-idle CPU;kernel/src/sched.rs), and a CPU that leaves a user address space reloads CR3 – to a successor or, on exit, the kernel PML4 (exit_current) – which in the current no-PCID scheduler evicts that space’s non-global user translations (user PTEs are neverGLOBAL). Process removal / non-residency is therefore what makes a separate teardown shootdown unnecessary (see the “Live address space targeted externally” and “Borrowed mapping in a dying address space” rows above): theDropwalk frees the owned leaves, committed anonymous frames, and table frames with only a local reload.
capos-lib/src/mem_transition.rs models this ordering contract as a small state
machine and, under a fault injected at the first, a middle, and the final commit
step plus a rollback-step fault, proves the modelled world always reconciles PTE
presence/permission, the tracking record, the virtual and frame-grant charges,
frame ownership, and the deferred-free list – or fail-stops – never returning a
silently inconsistent state. The tests also encode the pre-fix orderings
(detach-metadata-before-PTE-removal, fallible rollback) and show the
reconciliation predicate rejects the states they can produce, so the guarantee
is not vacuous.
Code Map
capos-lib/src/mem_failure.rs- host-testableMemoryExhaustiontaxonomy: class-to-token/reason mapping and canonical exhaustion-message builder.capos-lib/src/mem_transition.rs- host-testable model of the transition failure-atomicity ordering contract (preflight/infallible-commit, infallible rollback, shootdown-gated deferred free) with fault-injection tests.capos-lib/src/vm_reservation.rs- host-testable anonymous reservation, borrowed-mapping provenance, and separate held-backing/borrowed-mapping charge model. Deliberately wrong overlap and wrong-object-unmap variants make its rejection and survival assertions non-vacuous.kernel/src/cap/mod.rs-memory_exhaustion_errorbuilds the normalizedoverloadedcapnp::Errorfor aMemoryExhaustionclass.capos-lib/src/frame_bitmap.rs- host-testable physical frame bitmap core.capos-lib/src/cap_table.rs- capability holds and per-processResourceLedgerframe-grant accounting.capos-lib/src/frame_ledger.rs- bounded frame-grant helper retained for host tests.kernel/src/mem/frame.rs- Limine memory-map integration and global frame allocator wrapper.kernel/src/mem/heap.rs- kernel heap setup.kernel/src/mem/paging.rs- kernel remap,AddressSpace, page mapping, VM-cap page tracking, user copy helpers, and the infallible-after-preflight PTE primitives (unmap_present,protect_present,remap_present).kernel/src/mem/validate.rs- shared user-address arithmetic and bounds helper; page-table validation lives on the lockedAddressSpace.kernel/src/cap/frame_alloc.rs- FrameAllocator capability and cleanup.demos/memoryobject-shared-parent/anddemos/memoryobject-shared-child/- QEMU shared MemoryObject smoke.tools/qemu-memoryobject-shared-smoke.sh- transcript checks for the shared MemoryObject smoke.kernel/src/cap/virtual_memory.rs- VirtualMemory capability.kernel/src/spawn.rs- ELF, stack, and TLS user mappings.kernel/src/arch/x86_64/smap.rs- SMEP/SMAP setup and the direct-user-access guard; current user-buffer copies translate under the address-space lock and access frames through HHDM instead.
Validation
cargo test-libcovers frame bitmap, frame ledger, ELF parser, cap-table pure logic, and themem_failurememory-exhaustion taxonomy (every class isoverloaded, tokens are stable and unique, messages carry their token).make run-untrusted-exhaustionforces the virtual-quota, physical-commit, result-cap-publication, and ProcessSpawner global-frame-pressure boundaries in QEMU and asserts each returns its typed token while an already-valid console call still completes. The global-pressure case runs through the proof-onlyrun-untrusted-exhaustion-spawn-oom-faultsibling; the core pass continues to boot an ordinary QEMU kernel.cargo miri-libruns host-testablecapos-libtests under Miri when installed.make kani-libproves the bounded mandatory frame-bitmap, stale-handle, cap-slot/frame-grant accounting, and transfer preflight fail-closed invariants when Kani is installed.make run-smokevalidates ELF mapping, process teardown, TLS, and clean shell-led halt.make run-spawnvalidates MemoryObject-backed FrameAllocator cleanup, VirtualMemory reserve/commit/decommit/VM_PROT_NONE/quota/release smoke, and runtime spawn checks.make run-memoryobject-sharedvalidates a parent allocating and mapping a MemoryObject, typed refusal of an object map over a live anonymous reservation, typed refusal when a different object attempts the unmap, survival of the incumbent reservation and mapping after those refusals, transfer to a child, a child write through the same backing pages, clean unmap on both sides, and clean halt.make run-ipc-zerocopyvalidates the multi-message shared point-to-point buffer pattern at the substrate level: a producer transfers oneMemoryObjectto the consumer and then exchanges four record payloads through the shared mapping while endpoint CALLs carry only sequence numbers and checksums. This is a substrate proof, not the production data-plane shape: typedSharedBufferwith explicit producer/consumer ring metadata, notification primitives, and consuming service APIs (File.readBuf,BlockDevice.readBlocks, NIC RX/TX rings) are tracked under Open Work.make run-spawnvalidates ELF load failure rollback and frame exhaustion handling throughProcessSpawner.
Open Work
- Extend frame-grant accounting only if future DMA pinning or service-owned shared-buffer pools need authority beyond held MemoryObject caps and live borrowed mappings.
- Implement the accepted Shared Mapping Identity and Object Pins rules before shared WaitSet/ParkSpace or service-owned shared-buffer paths keep backing stable beyond a single locked copy/read. DMA retains its separate authority and quiesce contract.
- Add file, block, network, and DMA service APIs that use MemoryObject-backed SharedBuffer caps for zero-copy data paths.
- Add DMA isolation and device memory capability boundaries before userspace drivers.
- Add huge-page handling only with explicit ownership and teardown rules.