Skip to main content

strat9_kernel/memory/
mod.rs

1// Memory management module
2//
3// See also: [Memory Management Guide](https://strat9-os.org/strat9-os-docs/memory-model.html)
4// for architecture diagrams (buddy allocator, slab, COW, page tables).
5
6pub mod address_space;
7pub mod block;
8pub mod block_meta;
9pub mod boot_alloc;
10pub mod buddy;
11pub mod cow;
12pub mod frame;
13pub mod heap;
14pub mod mapping_index;
15pub mod ownership;
16pub mod paging;
17pub mod region_cap;
18pub mod userslice;
19pub mod vmalloc;
20pub mod zone;
21
22use crate::{
23    boot::entry::MemoryRegion, capability::CapId, process::get_task_by_pid, sync::IrqDisabledToken,
24};
25use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
26use spin::Once;
27
28/// Higher Half Direct Map offset.
29/// Set by Limine entry (non-zero) or left at 0 for BIOS/identity-mapped boot.
30/// All physical-to-virtual conversions must add this offset.
31static HHDM_OFFSET: AtomicU64 = AtomicU64::new(0);
32
33/// Store the HHDM offset (call once, early in boot)
34pub fn set_hhdm_offset(offset: u64) {
35    HHDM_OFFSET.store(offset, Ordering::Relaxed);
36}
37
38/// Get the current HHDM offset
39pub fn hhdm_offset() -> u64 {
40    HHDM_OFFSET.load(Ordering::Relaxed)
41}
42
43/// Convert a physical address to a virtual address using the HHDM offset
44#[inline]
45pub fn phys_to_virt(phys: u64) -> u64 {
46    phys.wrapping_add(HHDM_OFFSET.load(Ordering::Relaxed))
47}
48
49/// Convert a virtual address back to a physical address (reverse of phys_to_virt)
50#[inline]
51pub fn virt_to_phys(virt: u64) -> u64 {
52    virt.wrapping_sub(HHDM_OFFSET.load(Ordering::Relaxed))
53}
54
55/// Initialize the memory management subsystem
56pub fn init_memory_manager(memory_regions: &[MemoryRegion]) {
57    buddy::init_buddy_allocator(memory_regions);
58    // Race/corruption diagnostic: register slab lock for E9 LOCK-A/LOCK-R traces.
59    heap::debug_register_slab_trace();
60}
61
62/// Initialize copy-on-write metadata.
63pub fn init_cow_subsystem(_memory_regions: &[MemoryRegion]) {}
64
65static OWNERSHIP_TABLE: Once<OwnershipTable> = Once::new();
66static GLOBAL_MAPPING_INDEX: Once<MappingIndex> = Once::new();
67static MEMORY_REGION_REGISTRY: Once<MemoryRegionRegistry> = Once::new();
68
69// Re-exports
70pub use crate::sync::with_irqs_disabled;
71pub use address_space::{
72    kernel_address_space, AddressSpace, EffectiveMapping, VmaFlags, VmaPageSize, VmaType,
73};
74pub use block::{
75    BlockHandle, BuddyReserved, Exclusive, MappedExclusive, MappedShared, PhysBlock, Released,
76};
77pub use block_meta::{get_block_meta, resolve_handle};
78pub use buddy::{
79    buddy_alloc_fail_counts_snapshot, get_allocator, poison_quarantine_pages_snapshot,
80};
81pub use frame::{
82    block_phys_has_poison_guard, frame_meta_debug_snapshot, get_meta, get_meta_slot,
83    invoke_vtable_on_last_ref, invoke_vtable_on_unmap, meta_generation_matches, meta_guard,
84    AllocError, FrameAllocOptions, FrameAllocator, FrameMeta, FrameMetaVtable, FramePurpose,
85    FreeListLink, MetaSlot, PhysFrame, DEFAULT_FRAME_META_VTABLE, META_SLOT_REFCOUNT_BYTE_OFFSET,
86};
87pub use mapping_index::{MappingIndex, MappingRef};
88pub use ownership::{BlockState, OwnerEntry, OwnerError, OwnershipTable, RemoveRefResult};
89pub use region_cap::{
90    MemoryRegionRegistry, PublicMemoryRegionInfo, RegionCapError, ReleaseRegionResult,
91};
92pub use userslice::{UserSliceError, UserSliceRead, UserSliceReadWrite, UserSliceWrite};
93
94/// Returns the global ownership table used by the memory runtime.
95pub fn ownership_table() -> &'static OwnershipTable {
96    OWNERSHIP_TABLE.call_once(OwnershipTable::new)
97}
98
99/// Returns the global reverse mapping index used by the memory runtime.
100pub fn mapping_index() -> &'static MappingIndex {
101    GLOBAL_MAPPING_INDEX.call_once(MappingIndex::new)
102}
103
104/// Returns the global public memory-region registry.
105pub fn memory_region_registry() -> &'static MemoryRegionRegistry {
106    MEMORY_REGION_REGISTRY.call_once(MemoryRegionRegistry::new)
107}
108
109/// Allocates a fresh internal mapping capability identifier.
110pub fn allocate_mapping_cap_id() -> CapId {
111    CapId::new()
112}
113
114/// Records that `cap_id` now names `handle` in the ownership table.
115pub fn register_mapping_identity(handle: BlockHandle, cap_id: CapId) {
116    match try_register_mapping_identity(handle, cap_id) {
117        Ok(_) | Err(OwnerError::CapAlreadyPresent) => {}
118        Err(error) => {
119            log::warn!(
120                "memory: failed to register block identity cap={} handle={:#x}/{}: {:?}",
121                cap_id.as_u64(),
122                handle.base.as_u64(),
123                handle.order,
124                error
125            );
126        }
127    }
128}
129
130/// Fallible variant of mapping identity registration for transactional callers.
131pub fn try_register_mapping_identity(handle: BlockHandle, cap_id: CapId) -> Result<(), OwnerError> {
132    ownership_table().ensure_ref(handle, cap_id).map(|_| ())
133}
134
135/// Releases a block back to the buddy allocator.
136///
137/// Lifecycle order:
138/// 1. Invoke per-block [`FrameMetaVtable::on_last_ref`] (once, for the head frame) :
139///    signals that the last shared ownership reference has been dropped.
140/// 2. Invoke per-page [`FrameMetaVtable::on_unmap`] hooks (once per constituent 4 KiB page)
141///    : signals that mappings are being torn down.
142/// 3. Return the block to buddy.  Poisoned frames ([`meta_guard::POISONED`]) are quarantined
143///    and not recycled ([`poison_quarantine_pages_snapshot`]).
144///
145/// # Hook ordering guarantee
146/// `on_last_ref` and `on_unmap` are called **before** the buddy allocator decides to recycle
147/// or quarantine.  Hooks that rely on the frame being recyclable may run pointlessly on
148/// poisoned blocks : they must therefore be idempotent and side-effect-safe.
149pub fn release_owned_block(block: PhysBlock<Released>) {
150    let handle = block.into_handle();
151    with_irqs_disabled(|token| {
152        let frame_phys = handle.base.as_u64();
153        let order = handle.order;
154
155        // 1. on_last_ref : once per block (head frame only).
156        frame::invoke_vtable_on_last_ref(x86_64::PhysAddr::new(frame_phys));
157
158        // 2. on_unmap : once per constituent page.
159        for i in 0..(1u64 << order) {
160            let p = x86_64::PhysAddr::new(frame_phys + i * frame::PAGE_SIZE);
161            frame::invoke_vtable_on_unmap(p);
162        }
163        buddy::free(
164            token,
165            PhysFrame {
166                start_address: handle.base,
167            },
168            order,
169        );
170    });
171}
172
173/// Removes `cap_id` from the ownership table entry associated with `handle`.
174pub fn unregister_mapping_identity(
175    handle: BlockHandle,
176    cap_id: CapId,
177) -> Option<PhysBlock<Released>> {
178    match ownership_table().remove_ref(handle, cap_id) {
179        Ok(RemoveRefResult::Freed(block)) => Some(block),
180        Ok(_) | Err(OwnerError::NotFound) | Err(OwnerError::CapNotFound) => None,
181        Err(error) => {
182            log::warn!(
183                "memory: failed to unregister mapping identity cap={} handle={:#x}/{}: {:?}",
184                cap_id.as_u64(),
185                handle.base.as_u64(),
186                handle.order,
187                error
188            );
189            None
190        }
191    }
192}
193
194/// Revokes every live mapping associated with `cap_id`.
195pub fn revoke_mapping_cap_id(cap_id: CapId) -> usize {
196    let mappings = mapping_index().lookup(cap_id);
197    let mut revoked = 0usize;
198
199    for mapping in mappings {
200        let Some(task) = get_task_by_pid(mapping.pid) else {
201            mapping_index().unregister(cap_id, mapping.pid, mapping.vaddr);
202            continue;
203        };
204
205        let address_space = task.process.address_space_arc();
206        match address_space.unmap_effective_mapping(mapping.vaddr.as_u64()) {
207            Ok(()) => {
208                revoked = revoked.saturating_add(1);
209            }
210            Err(error) => {
211                if address_space
212                    .effective_mapping_by_start(mapping.vaddr.as_u64())
213                    .is_none()
214                {
215                    mapping_index().unregister(cap_id, mapping.pid, mapping.vaddr);
216                } else {
217                    log::warn!(
218                        "memory: failed to revoke mapping cap={} pid={} vaddr={:#x}: {}",
219                        cap_id.as_u64(),
220                        mapping.pid,
221                        mapping.vaddr.as_u64(),
222                        error
223                    );
224                }
225            }
226        }
227    }
228
229    if revoked != 0 {
230        crate::arch::x86_64::tlb::shootdown_all();
231    }
232
233    revoked
234}
235
236/// Allocate `2^order` contiguous physical frames (raw, no zeroing).
237///
238/// **Deprecated** : use [`allocate_phys_contiguous`] for DMA / hardware-ring
239/// allocations where physical contiguity is the explicit requirement, or
240/// [`allocate_frame`] for single kernel-data frames.  This name remains for
241/// internal callers that pre-date the explicit-intent API.
242#[deprecated(
243    note = "use allocate_phys_contiguous() for DMA/contiguous allocations, \
244            or allocate_frame() for single kernel-data frames"
245)]
246#[inline]
247pub fn allocate_frames(token: &IrqDisabledToken, order: u8) -> Result<PhysFrame, AllocError> {
248    buddy::alloc(token, order)
249}
250
251/// Total pages handed out by [`allocate_phys_contiguous`] (in units of 4 KiB pages, summed across orders).
252///
253/// Incremented on every successful contiguous alloc; never decremented.
254/// Paired with [`CONTIGUOUS_FREE_PAGES`] to derive the live count.
255static CONTIGUOUS_ALLOC_PAGES: AtomicUsize = AtomicUsize::new(0);
256/// Total pages returned via [`free_phys_contiguous`].
257static CONTIGUOUS_FREE_PAGES: AtomicUsize = AtomicUsize::new(0);
258/// Total failed contiguous allocation attempts (fragmentation indicator).
259static CONTIGUOUS_ALLOC_FAIL_COUNT: AtomicUsize = AtomicUsize::new(0);
260
261/// Allocate a physically contiguous block of `2^order` pages.
262///
263/// Use when **physical contiguity** is required: DMA rings, MMIO-adjacent
264/// buffers, hardware tables, copy-on-write multi-page copies, kernel stacks
265/// ([`allocate_kernel_stack_frames`]), etc.
266///
267/// Do not use for general large kernel buffers : prefer [`allocate_kernel_virtual`]
268/// (virtually contiguous, physically fragmented) or [`allocate_frame`] for single pages.
269///
270/// **Telemetry:** increments [`CONTIGUOUS_ALLOC_PAGES`] on success and
271/// [`CONTIGUOUS_ALLOC_FAIL_COUNT`] on failure; [`CONTIGUOUS_FREE_PAGES`] on
272/// [`free_phys_contiguous`]. Those counters include **every** use of this
273/// allocate/free pair (DMA buffers, kernel stacks, COW multi-page blocks,
274/// etc.) : not only hardware DMA. Treat [`phys_contiguous_diag`] as overall
275/// buddy multi-page contiguous traffic.
276#[inline]
277pub(crate) fn allocate_phys_contiguous(
278    token: &IrqDisabledToken,
279    order: u8,
280) -> Result<PhysFrame, AllocError> {
281    match buddy::alloc(token, order) {
282        Ok(frame) => {
283            CONTIGUOUS_ALLOC_PAGES.fetch_add(1usize << order, Ordering::Relaxed);
284            Ok(frame)
285        }
286        Err(e) => {
287            CONTIGUOUS_ALLOC_FAIL_COUNT.fetch_add(1, Ordering::Relaxed);
288            Err(e)
289        }
290    }
291}
292
293/// Free `2^order` contiguous physical frames.
294///
295/// **Deprecated** : use [`free_phys_contiguous`] for blocks returned by
296/// [`allocate_phys_contiguous`], or [`free_frame`] for single frames.
297#[deprecated(
298    note = "use free_phys_contiguous() for blocks from allocate_phys_contiguous, \
299            or free_frame() for single frames"
300)]
301#[inline]
302pub fn free_frames(token: &IrqDisabledToken, frame: PhysFrame, order: u8) {
303    buddy::free(token, frame, order);
304}
305
306/// Free a physically contiguous block previously returned by
307/// [`allocate_phys_contiguous`].
308///
309/// DO NOT USE to free frames from [`allocate_frame`] : use [`free_frame`] instead.
310#[inline]
311pub(crate) fn free_phys_contiguous(token: &IrqDisabledToken, frame: PhysFrame, order: u8) {
312    CONTIGUOUS_FREE_PAGES.fetch_add(1usize << order, Ordering::Relaxed);
313    buddy::free(token, frame, order);
314}
315
316/// Snapshot of contiguous-physical-allocation telemetry.
317///
318/// Covers all [`allocate_phys_contiguous`] / [`free_phys_contiguous`] usage
319/// (DMA-style buffers, kernel stacks, COW multi-page blocks, etc.).
320pub struct PhysContiguousDiag {
321    pub pages_allocated: usize,
322    pub pages_freed: usize,
323    pub pages_live: usize,
324    pub alloc_fail_count: usize,
325}
326
327/// Read current contiguous-allocation telemetry without locking.
328///
329/// Counters are not limited to device DMA; any code path using
330/// [`allocate_phys_contiguous`] contributes (see module docs on that function).
331pub fn phys_contiguous_diag() -> PhysContiguousDiag {
332    let alloc = CONTIGUOUS_ALLOC_PAGES.load(Ordering::Relaxed);
333    let freed = CONTIGUOUS_FREE_PAGES.load(Ordering::Relaxed);
334    PhysContiguousDiag {
335        pages_allocated: alloc,
336        pages_freed: freed,
337        pages_live: alloc.saturating_sub(freed),
338        alloc_fail_count: CONTIGUOUS_ALLOC_FAIL_COUNT.load(Ordering::Relaxed),
339    }
340}
341
342/// Allocate physically contiguous pages for a kernel stack.
343///
344/// Kernel stacks need a contiguous physical backing in the current design
345/// because they are carved as a single block and directly accessed through the
346/// HHDM. This is distinct from DMA intent, so keep a dedicated API.
347#[inline]
348pub fn allocate_kernel_stack_frames(
349    token: &IrqDisabledToken,
350    order: u8,
351) -> Result<PhysFrame, AllocError> {
352    allocate_phys_contiguous(token, order)
353}
354
355/// Free pages previously returned by [`allocate_kernel_stack_frames`].
356#[inline]
357pub fn free_kernel_stack_frames(token: &IrqDisabledToken, frame: PhysFrame, order: u8) {
358    free_phys_contiguous(token, frame, order);
359}
360
361/// Allocate a single **zeroed** physical frame with `KernelData` purpose.
362///
363/// This is the standard allocation path for all kernel-internal frames.  It
364/// uses `FrameAllocOptions::new()` (zeroed = true, purpose = KernelData) and
365/// performs the UNUSED → 0 → 1 refcount CAS (Asterinas OSTD pattern).
366///
367/// For page-table node allocation use `BuddyFrameAllocator` (via paging.rs)
368/// or `FrameAllocOptions::new().purpose(FramePurpose::PageTable).allocate()`.
369/// For user-space frames use `FrameAllocOptions::new().purpose(FramePurpose::UserData)`.
370#[inline]
371pub fn allocate_frame(token: &IrqDisabledToken) -> Result<PhysFrame, AllocError> {
372    allocate_frame_for_purpose(token, FramePurpose::KernelData)
373}
374
375/// Allocate a single zeroed 4 KiB frame for an explicit purpose.
376///
377/// This wrapper keeps the default zeroing policy while allowing callers to
378/// select the ownership class that drives buddy migratetype selection.
379#[inline]
380pub fn allocate_frame_for_purpose(
381    token: &IrqDisabledToken,
382    purpose: FramePurpose,
383) -> Result<PhysFrame, AllocError> {
384    FrameAllocOptions::new().purpose(purpose).allocate(token)
385}
386
387/// Allocate a single zeroed 4 KiB frame intended for user-space memory.
388///
389/// The frame is tagged movable, which biases the buddy allocator toward the
390/// zone order intended to preserve scarce low memory for pinned kernel pages.
391#[inline]
392pub fn allocate_user_frame(token: &IrqDisabledToken) -> Result<PhysFrame, AllocError> {
393    allocate_frame_for_purpose(token, FramePurpose::UserData)
394}
395
396/// Free a single physical frame.
397/// Requires an `IrqDisabledToken` proving that IRQs are disabled on the calling CPU.
398/// The caller must ensure that the frame is not currently mapped anywhere and that
399/// the buddy allocator's internal metadata is consistent with the frame's state (e.g. refcount = 0).
400/// Prefer `free_frames()` for multi-frame blocks or when the buddy allocator's internal state may need to be updated.
401/// This raw path is kept for symmetry with `allocate_frames()` and for special cases where the caller manages zeroing and metadata explicitly.
402/// For standard single-frame deallocation, prefer `release_owned_block()` which also handles ownership table updates and safety checks.
403#[inline]
404pub fn free_frame(token: &IrqDisabledToken, frame: PhysFrame) {
405    buddy::free(token, frame, 0);
406}
407
408/// Allocate virtually contiguous kernel memory backed by fragmented physical
409/// pages.
410///
411/// This is the explicit large-allocation API for kernel callers that require a
412/// large virtually contiguous range but not physical contiguity.
413///
414/// Returned pointers are aligned to a **4 KiB** page boundary. For alignment
415/// stricter than one page, do not route through [`GlobalAlloc`] / large
416/// [`Layout`] on this heap : use a dedicated aligned mapping or slab path.
417#[inline]
418#[track_caller]
419pub fn allocate_kernel_virtual(
420    size: usize,
421    token: &IrqDisabledToken,
422) -> Result<*mut u8, vmalloc::VmallocError> {
423    vmalloc::vmalloc(size, token)
424}
425
426/// Free memory previously returned by [`allocate_kernel_virtual`].
427///
428/// Returns `true` if a mapping was released. `false` means nothing was freed
429/// (e.g. pointer not in the vmalloc arena or not a live allocation start).
430#[inline]
431pub fn free_kernel_virtual(ptr: *mut u8, token: &IrqDisabledToken) -> bool {
432    vmalloc::vfree(ptr, token)
433}
434
435/// Allocate a single zeroed 4 KiB frame.
436///
437/// Disables IRQs internally.  The frame is zeroed before being returned
438/// (`FrameAllocOptions::new()` defaults to zeroed = true).
439///
440/// This is a convenience wrapper for hardware drivers that need a single
441/// device-accessible page.  It carries no DMA-zone or address-range
442/// guarantee : the frame may come from any zone.  For strict DMA
443/// requirements (e.g. <16 MiB for ISA DMA), allocate from the DMA zone
444/// directly via `FrameAllocOptions`.
445pub fn allocate_zeroed_frame() -> Option<PhysFrame> {
446    with_irqs_disabled(|token| {
447        FrameAllocOptions::new()
448            .purpose(FramePurpose::KernelData)
449            .allocate(token)
450            .ok()
451    })
452}