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