strat9_kernel/memory/frame.rs
1//! Physical frame allocator abstraction.
2//!
3//! ## MetaSlot (per-frame metadata, issue #38)
4//!
5//! Each 4 KiB physical frame has a **dedicated 64-byte [`MetaSlot`]**
6//! in a separate contiguous array (initialized by [`init_metadata_array`]). Buddy
7//! free-list [`FreeListLink`] nodes, reference counts, purpose flags,
8//! [`meta_guard`] bits, a per-allocation **generation** counter, and an optional
9//! [`FrameMetaVtable`] live here : **not** in the mapped page bytes, so mappings
10//! see a pristine payload.
11//!
12//! ## Revue / invariants (issue #38)
13//!
14//! - **Pas de métadonnées dans la charge utile** : les liens buddy sont dans
15//! [`FreeListLink`], jamais écrits comme « faux pointeurs » dans les 4 KiB mappés.
16//! - **`generation`** : incrémentée uniquement par [`MetaSlot::note_new_allocation_epoch`]
17//! après un `CAS` réussi dans [`FrameAllocOptions::allocate`]. Ne pas utiliser
18//! [`MetaSlot::set_generation`] sauf bootstrap/tests : sinon les schémas « généalogiques »
19//! deviennent incohérents.
20//! - **`meta_guard::POISONED` vs `frame_flags::POISONED`** : deux espaces (bits dédiés
21//! `guard` vs flags logiques). Pour marquer une frame corrompue, préférer
22//! [`MetaSlot::mark_poisoned`] qui pose les deux.
23//! - **`vtable_ref` / `try_vtable_ref`** : bits `0` => défaut ; bits non alignés ou invalides
24//! => défaut (pas d’UB). Les pointeurs alignés doivent désigner une [`FrameMetaVtable`] `'static`
25//! valide lorsqu’ils sont enregistrés par le noyau.
26//! - **Cache order-0** : `buddy::alloc(0)` peut servir depuis le cache local ; le chemin
27//! [`FrameAllocOptions::allocate`] applique quand même le CAS + epoch sur la même frame.
28
29use crate::{arch::xshim::PhysAddr, memory::boot_alloc::BootAllocator, sync::IrqDisabledToken};
30use core::{
31 mem::{self, offset_of},
32 ptr,
33 sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering},
34};
35
36// ==============================================================================
37// FrameAllocOptions (Asterinas OSTD pattern)
38// ==============================================================================
39//
40// DESIGN NOTES : why this wrapper exists:
41//
42// * In Asterinas OSTD, `FrameAllocOptions::new()` defaults to `zeroed: true`.
43// This means callers can never accidentally hand out a frame that still holds
44// data from a previous lifetime. The only way to skip zeroing is an
45// explicit `.zeroed(false)` call at the site that *knows* it is safe to do
46// so (e.g. a frame that will be fully overwritten before any read).
47//
48// * The critical failure mode we are fixing:
49// `BuddyFrameAllocator::allocate_frame` (used by `OffsetPageTable` when it
50// needs a new intermediate page-table node) was returning raw, unzeroed
51// frames. A freshly-split buddy block can contain bytes left behind by the
52// slab allocator (POISON_BYTE = 0xDE) or by whatever previously lived in
53// that memory. The CPU page-table walker reads all 512 entries of every
54// intermediate node it traverses. A random non-zero entry is decoded as a
55// valid PTE pointing to an arbitrary physical address : which explains why
56// RIP (the first fetch address the CPU tries after entering Ring 3) changes
57// on every boot.
58//
59// * The `flags` field mirrors OSTD's per-frame metadata: we stamp the purpose
60// (kernel / user / page-table) into `FrameMeta::flags` atomically using
61// `Ordering::Release` so that any CPU that later reads the frame through
62// `get_meta` observes the correct flags.
63//
64// * Refcount state machine (OSTD-style, fully enforced):
65//
66// `buddy.rs` maintains the invariant: free-list frame ⟹ refcount == REFCOUNT_UNUSED.
67// `mark_block_free()` stamps REFCOUNT_UNUSED; `mark_block_allocated()` leaves it
68// untouched. `FrameAllocOptions::allocate()` performs CAS(REFCOUNT_UNUSED -> 1)
69// as a fail-fast corruption check before publishing the frame as live:
70//
71// buddy alloc ▶ optional zero ▶ set flags ▶ CAS(UNUSED -> 1) ▶ live
72
73/// Sentinel refcount for a frame that is in the buddy free list.
74///
75/// Mirrors `REF_COUNT_UNUSED` in Asterinas OSTD `meta.rs`.
76///
77/// `buddy.rs` stamps this value in `mark_block_free()` and leaves it intact in
78/// `mark_block_allocated()`. `FrameAllocOptions::allocate()` performs
79/// `CAS(REFCOUNT_UNUSED -> 1)` to atomically claim the frame and detect any
80/// double-free / free-list corruption.
81pub const REFCOUNT_UNUSED: u32 = u32::MAX;
82
83/// Options controlling how a physical frame is allocated.
84///
85/// The default configuration (`FrameAllocOptions::new()`) produces a
86/// **zeroed** frame. Callers that need a non-zeroed frame (e.g. DMA buffers
87/// that are immediately filled by hardware, or frames that will be fully
88/// overwritten before any read) must explicitly call `.zeroed(false)`.
89///
90/// # Example
91///
92/// ```ignore
93/// // Allocate a zeroed page-table frame (the safe default).
94/// let frame = FrameAllocOptions::new()
95/// .purpose(FramePurpose::PageTable)
96/// .allocate(token)?;
97///
98/// // Allocate a user-data frame without zeroing (caller guarantees it will
99/// // be fully overwritten, e.g. by an ELF segment load).
100/// let frame = FrameAllocOptions::new()
101/// .zeroed(false)
102/// .purpose(FramePurpose::UserData)
103/// .allocate(token)?;
104/// ```
105pub struct FrameAllocOptions {
106 /// Whether the frame content should be zeroed before being returned.
107 ///
108 /// Defaults to `true`. Setting this to `false` is only safe when the
109 /// caller guarantees the frame will be fully written before any read.
110 zeroed: bool,
111 /// The logical purpose of the frame, encoded as `frame_flags` bits.
112 purpose_flags: u32,
113}
114
115/// Describes the intended purpose of an allocated frame.
116///
117/// Purpose is written into `FrameMeta::flags` with `Ordering::Release` so
118/// that any concurrent reader of the metadata (e.g. a TLB-shootdown handler
119/// deciding whether a frame holds a page-table node) sees a consistent view.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum FramePurpose {
122 /// Frame will hold a kernel page-table node (PML4/PDPT/PD/PT).
123 ///
124 /// These frames MUST be zeroed : unzeroed page-table nodes are the primary
125 /// source of non-deterministic RIP at Ring 3 transition.
126 PageTable,
127 /// Frame belongs to kernel address-space (e.g. heap, stack, metadata).
128 KernelData,
129 /// Frame belongs to a user-space address-space (anonymous or file-backed).
130 UserData,
131 /// Caller-managed; raw flags are passed through unchanged.
132 Custom(u32),
133}
134
135impl FramePurpose {
136 fn to_flags(self) -> u32 {
137 match self {
138 // Page-table frames are always kernel-owned.
139 Self::PageTable => frame_flags::KERNEL | frame_flags::ALLOCATED,
140 Self::KernelData => frame_flags::KERNEL | frame_flags::ALLOCATED,
141 Self::UserData => frame_flags::USER | frame_flags::ALLOCATED | frame_flags::MOVABLE,
142 Self::Custom(f) => f | frame_flags::ALLOCATED,
143 }
144 }
145
146 /// Returns `true` if this purpose requires zeroing regardless of the
147 /// `zeroed` option. Page-table nodes must always be zeroed.
148 pub fn requires_zero(self) -> bool {
149 matches!(self, Self::PageTable)
150 }
151}
152
153impl Default for FrameAllocOptions {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl FrameAllocOptions {
160 /// Creates allocation options with safe defaults:
161 /// - `zeroed = true`
162 /// - purpose = `KernelData`
163 pub fn new() -> Self {
164 Self {
165 zeroed: true,
166 purpose_flags: FramePurpose::KernelData.to_flags(),
167 }
168 }
169
170 /// Override the zero-initialisation policy.
171 ///
172 /// # Safety contract (enforced by convention, not the type system)
173 ///
174 /// If `zeroed` is set to `false`, the caller MUST fully overwrite every
175 /// byte of the frame before allowing any other CPU or subsystem to read it.
176 /// Violating this rule is a memory-safety hazard: stale bytes in an
177 /// intermediate page-table node cause the CPU to follow arbitrary PTEs.
178 pub fn zeroed(mut self, zeroed: bool) -> Self {
179 self.zeroed = zeroed;
180 self
181 }
182
183 /// Set the intended purpose of the frame.
184 ///
185 /// `PageTable` purpose forces zeroing even if `.zeroed(false)` was called.
186 pub fn purpose(mut self, p: FramePurpose) -> Self {
187 self.purpose_flags = p.to_flags();
188 // Page-table nodes must always be zeroed : override any caller setting.
189 if p.requires_zero() {
190 self.zeroed = true;
191 }
192 self
193 }
194
195 /// Allocate a single 4 KiB frame according to the configured options.
196 ///
197 /// The allocation path is:
198 ///
199 /// 1. Ask the buddy allocator for an order-0 frame (exclusive ownership is
200 /// guaranteed by the buddy's own bitmap + free-list discipline).
201 /// 2. Optionally zero the 4 KiB frame contents via the HHDM.
202 /// 3. Stamp `FrameMeta::flags` with the purpose flags using `Release`
203 /// ordering.
204 /// 4. Store `refcount = 1` with `Release` ordering so any later `Acquire`
205 /// load of the refcount observes the fully-initialised metadata and
206 /// (if zeroed) zeroed content.
207 ///
208 /// # Sentinel handoff: `CAS(REFCOUNT_UNUSED -> 1)`
209 ///
210 /// `buddy.rs` maintains the invariant that every frame on the free list has
211 /// `refcount == REFCOUNT_UNUSED`. `mark_block_allocated()` leaves this
212 /// sentinel intact, so the frame arriving here still carries `REFCOUNT_UNUSED`.
213 ///
214 /// The CAS atomically claims the frame and acts as a fail-fast corruption
215 /// check: if the same frame appears twice in the buddy free list (double-free
216 /// or metadata corruption), the second allocation attempt will observe a
217 /// refcount of `1` (set by the first allocation) and panic immediately rather
218 /// than silently aliasing memory.
219 pub fn allocate(self, token: &IrqDisabledToken) -> Result<PhysFrame, AllocError> {
220 let migratetype = if self.purpose_flags & frame_flags::MOVABLE != 0 {
221 crate::memory::zone::Migratetype::Movable
222 } else {
223 crate::memory::zone::Migratetype::Unmovable
224 };
225
226 // Step 1 : exclusive frame from the buddy allocator.
227 let frame = crate::memory::buddy::alloc_migratetype(token, 0, migratetype)?;
228 let phys = frame.start_address.as_u64();
229
230 // SAFETY: `get_meta` panics only if `phys` is out-of-bounds, which
231 // would be a buddy-level invariant violation (it returned an address
232 // beyond the metadata array). That is a kernel bug, not UB here.
233 let meta = get_meta(frame.start_address);
234
235 // Step 2 : zero the frame content if required.
236 //
237 // The zeroing MUST happen before the `Release` store of `refcount = 1`
238 // (step 4) so that any thread performing an `Acquire` load of the
239 // refcount and then reading frame bytes observes zeros.
240 //
241 // For `FramePurpose::PageTable` this is unconditional: the CPU's
242 // page-table walker reads all 512 entries of every intermediate node it
243 // visits. Stale non-zero bytes would be decoded as valid PTEs pointing
244 // to arbitrary physical addresses, producing a non-deterministic RIP on
245 // Ring 3 entry (the root cause of the original bug).
246 //
247 // SAFETY: `phys_to_virt(phys)` is a valid HHDM address covering exactly
248 // `PAGE_SIZE` bytes. The buddy allocator guarantees we have exclusive
249 // ownership of these bytes for the duration of this function.
250 if self.zeroed {
251 unsafe {
252 ptr::write_bytes(
253 crate::memory::phys_to_virt(phys) as *mut u8,
254 0,
255 PAGE_SIZE as usize,
256 );
257 }
258 }
259
260 // Step 3 : stamp purpose flags with `Release` ordering.
261 //
262 // Any reader that subsequently loads `refcount` with `Acquire` (step 4)
263 // is guaranteed to observe these flags as well.
264 meta.flags.store(self.purpose_flags, Ordering::Release);
265 meta.set_order(0);
266
267 // Step 4 : claim the frame and publish it as live.
268 //
269 // CAS(REFCOUNT_UNUSED -> 1): atomically transitions the frame from the
270 // buddy free-list sentinel to a live, exclusively-owned frame. The
271 // `AcqRel` success ordering ensures steps 2 and 3 happen-before any
272 // `Acquire` load of this refcount by another CPU, and also observes
273 // the buddy's `Release` store of REFCOUNT_UNUSED.
274 //
275 // Failure means the frame's refcount was not REFCOUNT_UNUSED : either
276 // the frame is still live (double-alloc) or the buddy free list is
277 // corrupt (double-free). Both are kernel bugs; panic immediately.
278 meta.cas_refcount(REFCOUNT_UNUSED, 1)
279 .unwrap_or_else(|actual| {
280 panic!(
281 "buddy corruption: frame {:#x} refcount is {:#x} (expected REFCOUNT_UNUSED); \
282 double-free or free-list corruption",
283 phys, actual,
284 )
285 });
286
287 // New live epoch: default vtable, clear guard bits, bump generation (issue #38).
288 meta.note_new_allocation_epoch();
289
290 Ok(frame)
291 }
292}
293
294pub const PAGE_SIZE: u64 = 4096;
295pub const FRAME_META_ALIGN: usize = 64;
296pub const FRAME_META_SIZE: usize = 64;
297pub const FRAME_META_LINK_NONE: u64 = u64::MAX;
298
299/// Guard bits stored in [`MetaSlot::guard`] (issue #38 : extensible without touching page bytes).
300///
301/// Distinct from [`frame_flags::POISONED`] (logical frame state in `flags`).
302pub mod meta_guard {
303 /// No guard condition asserted.
304 pub const NONE: u32 = 0;
305 /// Frame must not be exposed as a userspace mapping (kernel / debug).
306 pub const KERNEL_ONLY: u32 = 1 << 0;
307 /// Slot marked poisoned after detected corruption (never recycle blindly).
308 pub const POISONED: u32 = 1 << 31;
309}
310
311/// Persistent flags stored in [`MetaSlot`] / [`FrameMeta`].
312pub mod frame_flags {
313 /// The frame is allocated
314 pub const ALLOCATED: u32 = 1 << 8;
315 /// The frame is free.
316 pub const FREE: u32 = 1 << 9;
317 /// The frame is reserved for the kernel.
318 pub const KERNEL: u32 = 1 << 10;
319 /// The frame belongs to user space.
320 pub const USER: u32 = 1 << 11;
321 /// The frame is poisoned and must not be recycled as-is.
322 pub const POISONED: u32 = 1 << 12;
323 /// The frame belongs to a movable page class.
324 pub const MOVABLE: u32 = 1 << 13;
325 /// Frame eligible for copy-on-write.
326 pub const COW: u32 = 1 << 0;
327 /// Shared frame of type DLL, never COW.
328 pub const DLL: u32 = 1 << 1;
329 /// Anonymous frame.
330 pub const ANONYMOUS: u32 = 1 << 2;
331 /// Frame is pinned for DMA transfer : buddy must not recycle.
332 pub const DMA: u32 = 1 << 14;
333}
334
335/// Buddy free-list link storage (intrusive list nodes live in [`MetaSlot`], not in frame bytes).
336///
337/// `AtomicU64` matches the rest of the metadata slot’s atomic story and keeps the public
338/// [`MetaSlot`] API safe if list helpers are ever used without the buddy spinlock. Today
339/// `buddy.rs` mutates these fields only while holding the global buddy lock, so plain
340/// `Cell<u64>` would suffice for ordering; that would be a micro-optimization if profiling shows
341/// hot contention here.
342#[repr(C)]
343pub struct FreeListLink {
344 pub(crate) next: AtomicU64,
345 pub(crate) prev: AtomicU64,
346}
347
348impl FreeListLink {
349 pub const fn new() -> Self {
350 Self {
351 next: AtomicU64::new(FRAME_META_LINK_NONE),
352 prev: AtomicU64::new(FRAME_META_LINK_NONE),
353 }
354 }
355}
356
357/// Custom vtable for frame-type-specific behavior (DMA teardown, device hooks, …).
358///
359/// Store a pointer as `u64` in [`MetaSlot::vtable`]; `0` selects [`DEFAULT_FRAME_META_VTABLE`].
360#[repr(C)]
361pub struct FrameMetaVtable {
362 /// Called when the last shared reference to the frame is dropped (`refcount` => 0 path).
363 ///
364 /// # When it runs
365 /// Invoked by [`release_owned_block`] **once** for the head frame of a block,
366 /// immediately after the last ownership reference is dropped and before any
367 /// per-page `on_unmap` hooks. It does **not** run for individual page unmappings
368 /// that leave the block pinned (e.g. one task unmapping while another still holds a pin).
369 ///
370 /// # Constraints
371 /// Invoked with IRQs **disabled** and without the buddy zone lock held.
372 /// MUST be: allocation-free, lock-free, and infallible.
373 pub on_last_ref: Option<fn(PhysAddr)>,
374 /// Called once per 4 KiB page when a mapping block is released to the allocator (unmap path).
375 ///
376 /// # When it runs
377 /// Invoked by [`release_owned_block`] **before** the buddy allocator decides whether
378 /// to recycle or quarantine the block. It therefore runs even for poisoned frames
379 /// that will be quarantined and never reused.
380 ///
381 /// # Constraints
382 /// Invoked with IRQs **disabled** and the buddy zone lock held on the caller's CPU.
383 /// MUST be:
384 /// - allocation-free (no heap, no buddy);
385 /// - lock-free (no spinlocks that might be held by the interrupted CPU);
386 /// - infallible (no panic, no unwrap).
387 pub on_unmap: Option<fn(PhysAddr)>,
388 /// Reserved for future hooks (keeps struct at 64 bytes for [`FRAME_META_SIZE`]).
389 pub reserved: [u64; 6],
390}
391
392/// Default vtable used when [`MetaSlot::vtable`] is `0`.
393pub static DEFAULT_FRAME_META_VTABLE: FrameMetaVtable = FrameMetaVtable {
394 on_last_ref: None,
395 on_unmap: None,
396 reserved: [0; 6],
397};
398
399const _: () = assert!(mem::size_of::<FrameMetaVtable>() == FRAME_META_SIZE);
400
401/// 64-byte cache-line metadata for one physical frame (issue #38).
402///
403/// Layout: free-list links + flags + refcount + optional vtable + generation + reserved tail
404/// for future guard bits / generational references without touching the page payload.
405///
406/// Use plain `#[repr(C)]` (not `align(64)` on the struct): `align(64)` would pad the **type
407/// size** to a multiple of 64 and can inflate `size_of` to 128. The metadata **array** is
408/// still allocated with [`FRAME_META_ALIGN`] so each slot stays cache-line aligned.
409///
410/// Field order matters: `vtable` immediately follows `free_link` so `AtomicU64` stays
411/// 8-byte aligned without hidden padding after `refcount` (which would inflate the struct
412/// to 72 bytes).
413#[repr(C)]
414pub struct MetaSlot {
415 pub free_link: FreeListLink,
416 /// `*const FrameMetaVtable` as bits; `0` means [`DEFAULT_FRAME_META_VTABLE`].
417 pub vtable: AtomicU64,
418 pub flags: AtomicU32,
419 pub order: AtomicU8,
420 /// Padding so `refcount` stays 4-byte aligned; if `order` widens or new fields are added,
421 /// re-check [`META_SLOT_REFCOUNT_BYTE_OFFSET`] / [`MetaSlot::REFCOUNT_BYTE_OFFSET`].
422 _reserved0: [u8; 3],
423 pub refcount: AtomicU32,
424 /// Bumps each time the frame is successfully claimed from the buddy free list
425 /// (see [`MetaSlot::note_new_allocation_epoch`]).
426 pub generation: AtomicU32,
427 /// Kernel-owned guard bits ([`meta_guard`]); independent of `frame_flags`.
428 pub guard: AtomicU32,
429 /// Low 16 bits: owner CPU id hint (issue #38); upper bits reserved / NUMA placeholder.
430 pub meta_aux: AtomicU32,
431 pub _reserved_tail: [u8; 16],
432}
433
434/// Byte offset of [`MetaSlot::refcount`] from the start of each metadata slot (layout contract).
435///
436/// Re-exported as [`crate::memory::META_SLOT_REFCOUNT_BYTE_OFFSET`]. Equals [`MetaSlot::REFCOUNT_BYTE_OFFSET`].
437pub const META_SLOT_REFCOUNT_BYTE_OFFSET: usize = offset_of!(MetaSlot, refcount);
438
439/// Backwards-compatible name for [`MetaSlot`].
440pub type FrameMeta = MetaSlot;
441
442impl MetaSlot {
443 /// Empty metadata for boot-time array initialization.
444 pub const fn new() -> Self {
445 Self {
446 free_link: FreeListLink::new(),
447 vtable: AtomicU64::new(0),
448 flags: AtomicU32::new(0),
449 order: AtomicU8::new(0),
450 _reserved0: [0; 3],
451 refcount: AtomicU32::new(0),
452 generation: AtomicU32::new(0),
453 guard: AtomicU32::new(0),
454 meta_aux: AtomicU32::new(0),
455 _reserved_tail: [0; 16],
456 }
457 }
458
459 /// Reset vtable/guard when returning a frame to the buddy free list (`buddy::set_block_meta`).
460 ///
461 /// Preserves [`meta_guard::POISONED`] so poisoned frames are not silently « healed » on free.
462 #[inline]
463 pub fn reset_with_free_list_meta(&self) {
464 self.set_vtable_bits(0);
465 let poison = self.get_guard() & meta_guard::POISONED;
466 self.guard.store(poison, Ordering::Release);
467 }
468
469 #[inline]
470 pub fn meta_aux_load(&self) -> u32 {
471 self.meta_aux.load(Ordering::Relaxed)
472 }
473
474 #[inline]
475 pub fn meta_aux_store(&self, v: u32) {
476 // CPU-id hint : no happens-before relationship needed; Relaxed is sufficient.
477 self.meta_aux.store(v, Ordering::Relaxed);
478 }
479
480 /// Byte offset of `refcount` from the start of [`MetaSlot`] (same as [`META_SLOT_REFCOUNT_BYTE_OFFSET`]).
481 pub const REFCOUNT_BYTE_OFFSET: usize = META_SLOT_REFCOUNT_BYTE_OFFSET;
482
483 /// After a successful `CAS(REFCOUNT_UNUSED => 1)` in [`FrameAllocOptions::allocate`],
484 /// start a new metadata epoch: default vtable, clear guards, bump generation.
485 ///
486 /// The generation bump uses [`Ordering::Release`] so another CPU that later
487 /// [`Acquire`]-loads [`Self::generation`] or pairs with the refcount hand-off sees this
488 /// epoch for genealogical use-after-free checks. [`Ordering::Relaxed`] would be enough only
489 /// if all such checks ran on the allocating CPU with no cross-CPU visibility requirement.
490 #[inline]
491 pub fn note_new_allocation_epoch(&self) {
492 self.set_vtable_bits(0);
493 self.guard.store(meta_guard::NONE, Ordering::Release);
494 self.generation.fetch_add(1, Ordering::Release);
495 }
496
497 #[inline]
498 pub fn get_guard(&self) -> u32 {
499 self.guard.load(Ordering::Acquire)
500 }
501
502 #[inline]
503 pub fn set_guard(&self, bits: u32) {
504 self.guard.store(bits, Ordering::Release);
505 }
506
507 #[inline]
508 pub fn fetch_or_guard(&self, bits: u32) -> u32 {
509 self.guard.fetch_or(bits, Ordering::AcqRel)
510 }
511
512 /// Returns `true` if [`meta_guard::POISONED`] is set.
513 #[inline]
514 pub fn is_guard_poisoned(&self) -> bool {
515 self.get_guard() & meta_guard::POISONED != 0
516 }
517
518 /// Marks both [`meta_guard::POISONED`] and [`frame_flags::POISONED`] (corruption / audit path).
519 #[inline]
520 pub fn mark_poisoned(&self) {
521 self.fetch_or_guard(meta_guard::POISONED);
522 self.set_flags(self.get_flags() | frame_flags::POISONED);
523 }
524
525 /// `(generation, guard_bits, vtable_bits)` for serial / shell diagnostics.
526 #[inline]
527 pub fn debug_snapshot(&self) -> (u32, u32, u64) {
528 (self.generation(), self.get_guard(), self.vtable_bits())
529 }
530
531 /// Raw vtable pointer bits (`0` = default).
532 #[inline]
533 pub fn vtable_bits(&self) -> u64 {
534 self.vtable.load(Ordering::Acquire)
535 }
536
537 /// Install a custom vtable pointer (must point to a `'static` [`FrameMetaVtable`]).
538 #[inline]
539 pub fn set_vtable_bits(&self, bits: u64) {
540 self.vtable.store(bits, Ordering::Release);
541 }
542
543 /// Resolved vtable reference (`0` bits map to [`DEFAULT_FRAME_META_VTABLE`]).
544 ///
545 /// Misaligned or otherwise invalid non-zero pointer bits fall back to the default vtable
546 /// (same as [`Self::try_vtable_ref`] returning `None`).
547 pub fn vtable_ref(&self) -> &'static FrameMetaVtable {
548 self.try_vtable_ref().unwrap_or(&DEFAULT_FRAME_META_VTABLE)
549 }
550
551 /// Like [`Self::vtable_ref`], but returns `None` if non-zero vtable bits are not aligned
552 /// to a [`FrameMetaVtable`] pointer (8-byte aligned).
553 pub fn try_vtable_ref(&self) -> Option<&'static FrameMetaVtable> {
554 let bits = self.vtable_bits();
555 if bits == 0 {
556 return Some(&DEFAULT_FRAME_META_VTABLE);
557 }
558 #[cfg(debug_assertions)]
559 debug_assert_eq!(
560 bits & 7,
561 0,
562 "MetaSlot::vtable_bits must be 8-byte aligned (got {bits:#x})"
563 );
564 if bits & 7 != 0 {
565 return None;
566 }
567 // `bits` is non-zero (checked above) and 8-byte aligned, so the pointer is non-null.
568 let ptr = bits as *const FrameMetaVtable;
569 // SAFETY: aligned, non-null; must point to a `'static` vtable when registered by the kernel.
570 unsafe { Some(&*ptr) }
571 }
572
573 /// Loads the allocation generation with [`Ordering::Acquire`], pairing with the
574 /// [`Ordering::Release`] bump in [`Self::note_new_allocation_epoch`] for cross-CPU checks.
575 #[inline]
576 pub fn generation(&self) -> u32 {
577 self.generation.load(Ordering::Acquire)
578 }
579
580 /// Overwrites the generation counter : **only** for boot-time init or tests.
581 ///
582 /// Normal allocations bump generation via [`MetaSlot::note_new_allocation_epoch`].
583 /// Arbitrary values break « generational » use-after-free checks.
584 #[inline]
585 pub fn set_generation(&self, g: u32) {
586 self.generation.store(g, Ordering::Release);
587 }
588
589 #[inline]
590 pub fn next(&self) -> u64 {
591 self.free_link.next.load(Ordering::Acquire)
592 }
593
594 #[inline]
595 pub fn set_next(&self, next: u64) {
596 self.free_link.next.store(next, Ordering::Release);
597 }
598
599 #[inline]
600 pub fn prev(&self) -> u64 {
601 self.free_link.prev.load(Ordering::Acquire)
602 }
603
604 #[inline]
605 pub fn set_prev(&self, prev: u64) {
606 self.free_link.prev.store(prev, Ordering::Release);
607 }
608
609 #[inline]
610 pub fn inc_ref(&self) {
611 self.refcount.fetch_add(1, Ordering::Relaxed);
612 }
613
614 #[inline]
615 pub fn dec_ref(&self) -> u32 {
616 self.refcount.fetch_sub(1, Ordering::Release)
617 }
618
619 #[inline]
620 pub fn get_refcount(&self) -> u32 {
621 self.refcount.load(Ordering::Acquire)
622 }
623
624 #[inline]
625 pub fn set_flags(&self, flags: u32) {
626 self.flags.store(flags, Ordering::Release);
627 }
628
629 #[inline]
630 pub fn get_flags(&self) -> u32 {
631 self.flags.load(Ordering::Acquire)
632 }
633
634 /// Atomic OR: set bits without racing with concurrent readers.
635 #[inline]
636 pub fn or_flags(&self, bits: u32) {
637 self.flags.fetch_or(bits, Ordering::AcqRel);
638 }
639
640 /// Atomic AND: clear bits without racing with concurrent readers.
641 #[inline]
642 pub fn and_flags(&self, bits: u32) {
643 self.flags.fetch_and(bits, Ordering::AcqRel);
644 }
645
646 #[inline]
647 pub fn get_order(&self) -> u8 {
648 self.order.load(Ordering::Acquire)
649 }
650
651 #[inline]
652 pub fn set_order(&self, order: u8) {
653 self.order.store(order, Ordering::Release);
654 }
655
656 #[inline]
657 pub fn set_refcount(&self, count: u32) {
658 self.refcount.store(count, Ordering::Release);
659 }
660
661 #[inline]
662 pub fn cas_refcount(&self, expect: u32, new: u32) -> Result<u32, u32> {
663 self.refcount
664 .compare_exchange(expect, new, Ordering::AcqRel, Ordering::Acquire)
665 }
666
667 #[inline]
668 pub fn reset_refcount(&self) {
669 self.set_refcount(0);
670 }
671
672 #[inline]
673 pub fn is_cow(&self) -> bool {
674 self.get_flags() & frame_flags::COW != 0
675 }
676
677 #[inline]
678 pub fn is_dll(&self) -> bool {
679 self.get_flags() & frame_flags::DLL != 0
680 }
681}
682
683const _: () = {
684 assert!(mem::size_of::<MetaSlot>() == FRAME_META_SIZE);
685 // Stride is `FRAME_META_SIZE`; the backing array is allocated with `FRAME_META_ALIGN`
686 // so each index maps to a cache-line-aligned slot even if `align_of::<MetaSlot>()` is 8.
687 assert!(mem::align_of::<MetaSlot>() <= FRAME_META_SIZE);
688 // `_reserved0` exists only to pad `order`+tail to 4 bytes before `refcount`; changing field
689 // sizes or order requires updating `META_SLOT_REFCOUNT_BYTE_OFFSET` and this assert.
690 assert!(META_SLOT_REFCOUNT_BYTE_OFFSET == 32);
691};
692
693/// The metadata array size for `ram_size` bytes, rounded up to the nearest page since each frame
694/// has a dedicated metadata entry.
695
696/// @param ram_size Total RAM size to be covered by the metadata (in bytes).
697///
698pub const fn metadata_size_for(ram_size: u64) -> u64 {
699 let frames = (ram_size / PAGE_SIZE) + if ram_size % PAGE_SIZE == 0 { 0 } else { 1 };
700 frames * FRAME_META_SIZE as u64
701}
702
703static METADATA_BASE_VIRT: AtomicU64 = AtomicU64::new(0);
704static METADATA_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
705
706/// Initialize the global metadata array for all physical frames.
707pub fn init_metadata_array(total_ram: u64, boot_alloc: &mut BootAllocator) {
708 crate::e9_mark!(b'X');
709 let frame_count = (total_ram / PAGE_SIZE) + if total_ram % PAGE_SIZE == 0 { 0 } else { 1 };
710 crate::e9_mark!(b'Y');
711 if frame_count == 0 {
712 METADATA_BASE_VIRT.store(0, Ordering::Release);
713 METADATA_FRAME_COUNT.store(0, Ordering::Release);
714 return;
715 }
716
717 // Allocate the real metadata array from the boot allocator.
718 //
719 // This used to be stubbed because the vtable function pointers landed at
720 // identity-mapped addresses (#UD when called from the higher-half). The
721 // array itself only stores DATA (vtable bits = 0 → DEFAULT vtable
722 // resolved statically), so identity mapping of the array is safe: with
723 // hhdm_offset == 0, phys_to_virt(phys) == phys, which the active page
724 // tables map via the bootloader's identity map for all RAM.
725 // Function-pointer vtables (non-zero bits) remain FORBIDDEN until the
726 // identity-vtable issue is fixed : enforced by keeping vtable = 0 on all
727 // slots (MetaSlot::new) and by reset_with_free_list_meta.
728 let bytes = frame_count * FRAME_META_SIZE as u64;
729 // Metadata is written immediately, before map_all_ram can extend the HHDM.
730 // Verify actual reachability even when entered through another boot path.
731 let phys = match boot_alloc.try_alloc_accessible(bytes as usize, 64) {
732 Some(p) => p.as_u64(),
733 None => {
734 // Cannot back the metadata: keep it disabled (get_meta_slot will
735 // panic with a clear message rather than corrupt memory).
736 crate::serial_force_println!("[frame] metadata alloc failed: need {} bytes", bytes);
737 METADATA_BASE_VIRT.store(0, Ordering::Release);
738 METADATA_FRAME_COUNT.store(0, Ordering::Release);
739 return;
740 }
741 };
742 let virt = crate::memory::phys_to_virt(phys);
743 crate::e9_mark!(b'w');
744 // DEBUG: phys/virt of the array (LSB-first nibbles after 'P').
745 unsafe {
746 let mut shift = 0i32;
747 core::arch::asm!("out 0xe9, al", in("al") b'P', options(nomem, nostack));
748 while shift < 64 {
749 let nib = ((phys >> shift) & 0xF) as u8;
750 let c = if nib < 10 {
751 b'0' + nib
752 } else {
753 b'a' + nib - 10
754 };
755 core::arch::asm!("out 0xe9, al", in("al") c, options(nomem, nostack));
756 shift += 4;
757 }
758 core::arch::asm!("out 0xe9, al", in("al") b'\n', options(nomem, nostack));
759 }
760 // Zero the array so every slot starts as DEFAULT vtable / empty links.
761 // Chunked + E9-progress: a silent hang here (bad backing region, wrong
762 // mapping) would otherwise be invisible.
763 unsafe {
764 let dst = virt as *mut u8;
765 let chunk = 0x10_0000usize; // 1 MiB
766 let mut done = 0usize;
767 while done < bytes as usize {
768 let n = (bytes as usize - done).min(chunk);
769 core::ptr::write_bytes(dst.add(done), 0, n);
770 done += n;
771 crate::e9_mark!(b'.');
772 }
773 }
774 crate::e9_mark!(b'v');
775 // DEBUG: report where the array landed (raw E9, R=addr marker).
776 unsafe {
777 let mut shift = 0i32;
778 core::arch::asm!("out 0xe9, al", in("al") b'@', options(nomem, nostack));
779 while shift < 64 {
780 let nib = ((virt >> shift) & 0xF) as u8;
781 let c = if nib < 10 {
782 b'0' + nib
783 } else {
784 b'a' + nib - 10
785 };
786 core::arch::asm!("out 0xe9, al", in("al") c, options(nomem, nostack));
787 shift += 4;
788 }
789 core::arch::asm!("out 0xe9, al", in("al") b'\n', options(nomem, nostack));
790 }
791 METADATA_BASE_VIRT.store(virt, Ordering::Release);
792 METADATA_FRAME_COUNT.store(frame_count, Ordering::Release);
793 crate::serial_force_println!(
794 "[frame] metadata array @ {:#x} ({} frames, {} bytes)",
795 virt,
796 frame_count,
797 bytes
798 );
799}
800
801/// Get the [`MetaSlot`] for a given physical frame (same as [`get_meta_slot`]).
802#[inline]
803pub fn get_meta(phys: PhysAddr) -> &'static MetaSlot {
804 get_meta_slot(phys)
805}
806
807/// `(generation, guard_bits, vtable_bits)` for debugging (e.g. `serial_println!`).
808#[inline]
809pub fn frame_meta_debug_snapshot(phys: PhysAddr) -> (u32, u32, u64) {
810 get_meta_slot(phys).debug_snapshot()
811}
812
813/// Returns `true` if [`MetaSlot::generation`] matches `expected` for `phys` (epoch check for use-after-free guards).
814#[inline]
815pub fn meta_generation_matches(phys: PhysAddr, expected: u32) -> bool {
816 get_meta_slot(phys).generation() == expected
817}
818
819/// Returns `true` if any page in `[phys, phys + 2^order * PAGE_SIZE)` has [`MetaSlot::is_guard_poisoned`].
820///
821/// Returns `false` when the metadata array is not yet initialized (early-boot
822/// guard: called from `free_list_push` / `alloc_from_zone` during buddy setup
823/// before `init_metadata_array` has run).
824pub fn block_phys_has_poison_guard(frame_phys: u64, order: u8) -> bool {
825 if METADATA_BASE_VIRT.load(Ordering::Acquire) == 0 {
826 return false; // metadata not yet initialized : no poison possible
827 }
828 let n = 1u64 << order;
829 for i in 0..n {
830 let p = PhysAddr::new(frame_phys + i * PAGE_SIZE);
831 if get_meta_slot(p).is_guard_poisoned() {
832 return true;
833 }
834 }
835 false
836}
837
838/// Invokes `on_unmap` from the frame vtable, if any (issue #38 : unmap / release path).
839pub fn invoke_vtable_on_unmap(phys: PhysAddr) {
840 let m = get_meta_slot(phys);
841 let Some(vt) = m.try_vtable_ref() else {
842 return;
843 };
844 if let Some(f) = vt.on_unmap {
845 f(phys);
846 }
847}
848
849/// Invokes `on_last_ref` from the frame vtable, if any (last refcount drop).
850pub fn invoke_vtable_on_last_ref(phys: PhysAddr) {
851 let m = get_meta_slot(phys);
852 let Some(vt) = m.try_vtable_ref() else {
853 return;
854 };
855 if let Some(f) = vt.on_last_ref {
856 f(phys);
857 }
858}
859
860/// Preferred name matching the frame metadata design (issue #38).
861pub fn get_meta_slot(phys: PhysAddr) -> &'static MetaSlot {
862 let base = METADATA_BASE_VIRT.load(Ordering::Acquire);
863 let frame_count = METADATA_FRAME_COUNT.load(Ordering::Acquire);
864 assert!(base != 0, "frame metadata array is not initialized");
865
866 let pfn = phys.as_u64() / PAGE_SIZE;
867 assert!(pfn < frame_count, "frame metadata access out of bounds");
868
869 let byte_offset = pfn as usize * FRAME_META_SIZE;
870 // SAFETY: le tableau global couvre au moins `frame_count` entrées et reste
871 // vivant pendant toute la durée du noyau.
872 unsafe { &*((base as usize + byte_offset) as *const MetaSlot) }
873}
874
875/// Physical frame (4KB aligned physical memory)
876#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
877pub struct PhysFrame {
878 pub start_address: PhysAddr,
879}
880
881/// Performs the phys frame containing address operation.
882impl PhysFrame {
883 /// Create a PhysFrame containing the given physical address
884 pub fn containing_address(addr: PhysAddr) -> Self {
885 PhysFrame {
886 start_address: PhysAddr::new(addr.as_u64() & !0xFFF),
887 }
888 }
889
890 /// Create a PhysFrame from a 4KB-aligned address
891 pub fn from_start_address(addr: PhysAddr) -> Result<Self, ()> {
892 if addr.is_aligned(4096u64) {
893 Ok(PhysFrame {
894 start_address: addr,
895 })
896 } else {
897 Err(())
898 }
899 }
900
901 /// Create an inclusive range of frames
902 pub fn range_inclusive(start: PhysFrame, end: PhysFrame) -> FrameRangeInclusive {
903 FrameRangeInclusive { start, end }
904 }
905}
906
907/// Iterator over an inclusive range of physical frames
908pub struct FrameRangeInclusive {
909 pub start: PhysFrame,
910 pub end: PhysFrame,
911}
912
913/// Performs the iterator operation for FrameRangeInclusive.
914impl Iterator for FrameRangeInclusive {
915 type Item = PhysFrame;
916
917 /// Performs the next operation.
918 fn next(&mut self) -> Option<Self::Item> {
919 if self.start <= self.end {
920 let frame = self.start;
921 self.start.start_address += 4096u64;
922 Some(frame)
923 } else {
924 None
925 }
926 }
927}
928
929/// Frame allocation errors
930#[derive(Debug, Clone, Copy, PartialEq, Eq)]
931pub enum AllocError {
932 /// No memory available
933 OutOfMemory,
934 /// Invalid order (> MAX_ORDER)
935 InvalidOrder,
936 /// Invalid address alignment
937 InvalidAddress,
938}
939
940/// Frame allocator trait
941pub trait FrameAllocator {
942 /// Allocate `2^order` contiguous frames.
943 ///
944 /// Le token interdit les appels depuis un contexte où le verrou global de
945 /// l'allocateur pourrait être ré-entré par interruption.
946 fn alloc(&mut self, order: u8, token: &IrqDisabledToken) -> Result<PhysFrame, AllocError>;
947
948 /// Free `2^order` contiguous frames starting at frame.
949 fn free(&mut self, frame: PhysFrame, order: u8, token: &IrqDisabledToken);
950
951 /// Allocate a single frame (convenience method)
952 fn alloc_frame(&mut self, token: &IrqDisabledToken) -> Result<PhysFrame, AllocError> {
953 self.alloc(0, token)
954 }
955}