Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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 MemoryObject caps, and live borrowed MemoryObject mappings.
  • Process virtual-reservation usage covers reserved anonymous VM pages whether or not they are committed.
  • Committed VM_PROT_NONE pages 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 MemoryObject backing alive until unmapped or address-space teardown.
  • MemoryObject unmap/protect only succeeds for borrowed pages backed by the same object.
  • VirtualMemory caps 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.
  • VirtualMemory cannot reserve, map, commit, decommit, unmap, or protect the ring or CapSet pages.
  • VirtualMemory commit/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 AddressSpace lock.

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.

ClassTokenOriginEmitting boundary
Virtual quotavirtual-quota-exhaustedper-process virtual reservation budget (ResourceLedger::reserve_virtual_reservation_pages)VirtualMemory.reserve
Physical commitphysical-commit-exhaustedper-process frame-grant budget (ResourceLedger::reserve_frame_grant_pages)VirtualMemory.commit, FrameAllocator.allocFrame/allocContiguous, MemoryObject.map
Global frame pressureglobal-frame-pressuresystem-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 publicationresult-cap-publication-failedcaller 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 stateAuthority and record of truthAccounting recordSerialization
Physical frame availabilityKernel frame allocation through FrameAllocCap, VirtualMemoryCap, page-table allocation, and process construction; FrameBitmap is the allocation recordGlobal free-frame count in FrameBitmap; per-process charges are separateGlobal mem::frame::ALLOCATOR mutex in alloc_frame, alloc_contiguous, and try_free_frame
Anonymous virtual reservationAddress-space-bound VirtualMemoryCap; AddressSpace::anonymous_reservations records intervalsCaller ResourceLedger::virtual_reservation_pages_usedCaller Process::caps mutex during capability dispatch, then the target AddressSpace mutex through address_space_lock
Committed anonymous backingThe same VirtualMemoryCap; each AnonymousCommittedPage records frame, protection, and any pending shootdownCaller ResourceLedger::frame_grant_pages_usedCaller cap-table mutex plus the target AddressSpace mutex; global frame allocator lock is taken per allocation/free
Held MemoryObject backingA MemoryObjectCap cap-table slot holds MemoryObjectBacking by ArcThe holder’s cap-slot charge and frame_grant_pages_used, charged by CapTable::insert_with_epochHolder Process::caps mutex; backing lifetime is the last Arc, not a parallel counter
Borrowed MemoryObject mappingMemoryObjectCap plus caller address space; AddressSpace::borrowed_vm_pages records a backing-owner Arc per pageMapper frame_grant_pages_used for every live borrowed pageCaller cap-table mutex plus mapper AddressSpace mutex
User page-table residency and deferred reuseAddressSpace owns lower-half tables; the monotonic resident_cpu_mask records every CPU that has loaded the root; ShootdownCompletion records remote generationsNo second authority ledger; the bounded DEFERRED_COMPLETIONS queue is completion storageAddress-space mutex for user PTEs; SHOOTDOWN_LOCK orders generations; DEFERRED_COMPLETIONS has its own mutex
Private ParkSpace waiterProcess-local ParkSpaceCap; Process::park_waiters and the thread’s BlockReason are the waiter recordsOne existing thread record plus one reserved_park_cqes creditGlobal scheduler lock; range cleanup uses SchedulerLockSite::WakeUnblock

Anonymous VirtualMemory

BeforeOperation and afterLedger, completion, and cleanupPerforming symbols
Unreservedreserve creates a sparse AnonymousReservation; no PTE or frame is installedCharges 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)
UnreservedCompatibility map reserves, serializes the address into kernel reply storage, then commits; success is committed accessible or committed inaccessibleReserve 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 uncommittedcommit 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 inaccessibleprotect changes PTE permissions, removes a PTE for VM_PROT_NONE, or restores a retained frame from VM_PROT_NONEThe 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 reservationdecommit removes committed-page metadata and returns the range to reserved/uncommitted stateOn 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 pagesunmap removes the requested reservation range, splitting an interval when needed, and leaves it unreservedOn 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 externallyExternal termination removes a process only after it is nonresident, then drops its user mappings and page tablesterminate_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 exitsSelf-exit removes the current process, dispatches another context, and defers destruction of the old mappings and page tablesexit_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

BeforeOperation and afterLedger, completion, and cleanupPerforming symbols
Free framesFrameAllocator allocates zeroed backing, wraps it in MemoryObjectBacking, and publishes one result capResult-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 publishedAllocation, Arc creation, cap insertion, or transfer-result serialization fails; state returns to free framesPre-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 callerMemoryObject.map installs borrowed PTEs and records backing-owner Arcs; state becomes held plus borrowed-mappedCharges 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-mappedMemoryObject.protect changes permissions while preserving the same backing identityMapping 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-mappedMemoryObject.unmap removes PTEs and owner records; state returns to held but unmapped for that rangeThe 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 slotsCAP_OP_RELEASE, transfer rollback, or process cap-table teardown removes a holdCapTable::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 spaceAddressSpace::drop removes the user PTE/table tree, then drops per-page backing-owner holdsThe 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 pathLock and local actionRemote completion and reuse rulePerforming symbols
User 4 KiB map, unmap, or permission changeCaller holds the AddressSpace mutex; map_to, unmap, or update_flags is followed by the local mapper flushAddressSpace::shootdown_page snapshots the conservative resident-CPU mask and requests a generation from every other online CPU in that maskkernel/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 residencyBefore 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 supersetLater 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 freeMutation 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 releaseprocess_ring removes cap-table holds and stores their objects in bounded RingScratch release storagecap_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 mappingpaging::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 mappingKERNEL_PAGE_TABLE_LOCK covers top-level checks, intermediate-table walks, and PTE installation; the virtual window is the monotonic NEXT_KERNEL_MMIO_VADDR allocation recordEach 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

PathCurrent stability guaranteePerforming symbols
Shared bounds and copy primitiveuser_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 resultsThe 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 descriptorsDescriptor 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 readsThe 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 copiesScheduler 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 initializationThe 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

EventCurrent cleanupPerforming symbols
Anonymous VirtualMemory.unmap or decommitAfter 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.unmapThe 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 exitThe 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 dropRemoving 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 releaseThe 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 underlying Mapper call cannot fail: unmap clears only the leaf entry and leaves L2/L3/L4 intact (so it never frees a frame the metadata still references), update_flags rewrites an existing leaf, and the rollback map_to reuses the intermediate tables unmap left 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_page fails, the frame allocated for that page was never installed into a live PTE, so VirtualMemoryCap::commit_region frees 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_locked rejects 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 never GLOBAL). 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): the Drop walk 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-testable MemoryExhaustion taxonomy: 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_error builds the normalized overloaded capnp::Error for a MemoryExhaustion class.
  • capos-lib/src/frame_bitmap.rs - host-testable physical frame bitmap core.
  • capos-lib/src/cap_table.rs - capability holds and per-process ResourceLedger frame-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 locked AddressSpace.
  • kernel/src/cap/frame_alloc.rs - FrameAllocator capability and cleanup.
  • demos/memoryobject-shared-parent/ and demos/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-lib covers frame bitmap, frame ledger, ELF parser, cap-table pure logic, and the mem_failure memory-exhaustion taxonomy (every class is overloaded, tokens are stable and unique, messages carry their token).
  • make run-untrusted-exhaustion forces 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-only run-untrusted-exhaustion-spawn-oom-fault sibling; the core pass continues to boot an ordinary QEMU kernel.
  • cargo miri-lib runs host-testable capos-lib tests under Miri when installed.
  • make kani-lib proves the bounded mandatory frame-bitmap, stale-handle, cap-slot/frame-grant accounting, and transfer preflight fail-closed invariants when Kani is installed.
  • make run-smoke validates ELF mapping, process teardown, TLS, and clean shell-led halt.
  • make run-spawn validates MemoryObject-backed FrameAllocator cleanup, VirtualMemory reserve/commit/decommit/VM_PROT_NONE/quota/release smoke, and runtime spawn checks.
  • make run-memoryobject-shared validates 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-zerocopy validates the multi-message shared point-to-point buffer pattern at the substrate level: a producer transfers one MemoryObject to 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: typed SharedBuffer with 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-spawn validates ELF load failure rollback and frame exhaustion handling through ProcessSpawner.

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.