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}