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}