Skip to main content

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}