Skip to main content

strat9_kernel/memory/
heap.rs

1// Heap allocator: slab sub-allocator + VM-backed large-allocation path.
2//
3// Small allocations (effective size <= 2048 B) come from per-size-class slab
4// free lists.  Each slab class draws whole pages from the buddy allocator and
5// carves them into fixed-size blocks.  Freed blocks return to the slab free
6// list, so the buddy's page counter stabilises after warm-up instead of
7// growing on every tiny allocation.
8//
9// Large allocations (> 2048 B) go through the kernel vmalloc backend:
10// virtually contiguous, physically fragmented, and independent from
11// high-order physically contiguous buddy blocks.
12//
13// Lock ordering : SLAB_ALLOC (outer) may call the frame-allocation helpers.
14// Those helpers can hit a CPU-local cache (no global buddy lock) or fall back
15// to the global buddy lock as needed.
16
17use crate::{memory, sync::SpinLock};
18use core::{
19    alloc::{GlobalAlloc, Layout},
20    ptr,
21    sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
22};
23use x86_64::PhysAddr;
24
25// ---------------------------------------------------------------------------
26// Slab size classes
27// ---------------------------------------------------------------------------
28
29/// Slab block sizes chosen to bound internal fragmentation to ~25% worst-case
30/// (average ~12%) instead of 50% with pure power-of-two classes.
31///
32/// The progression follows a roughly 1.25× step above 64 bytes.  Below 64
33/// bytes the absolute waste of a 2× jump is small enough (max 32 bytes) to
34/// keep power-of-two boundaries, avoiding an explosion of size classes.
35///
36/// | Class range | Step      | Max waste |
37/// |-------------|-----------|-----------|
38/// | 8 to  64 B  | x2 / 1,5× | ≤ 32 B    |
39/// |64 to 256 B  | ~1.25×    | ≤ 64 B    |
40/// |256 to 2048 B| 1.25×     | ≤ 512 B   |
41
42const SLAB_SIZES: [usize; 26] = [
43    8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640, 768, 896,
44    1024, 1280, 1536, 1792, 2048,
45];
46const NUM_SLABS: usize = SLAB_SIZES.len();
47/// Allocations with effective size above this threshold bypass the slab.
48const MAX_SLAB_SIZE: usize = 2048;
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum KernelHeapBackend {
52    Slab,
53    Vmalloc,
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum KernelHeapAllocError {
58    InvalidLayout,
59    /// [`GlobalAlloc`] large path uses vmalloc, which only guarantees 4 KiB alignment.
60    AlignmentExceedsKernelPage {
61        align: usize,
62    },
63    SlabRefillFailed {
64        effective: usize,
65        class_size: usize,
66    },
67    Vmalloc(memory::vmalloc::VmallocError),
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct KernelHeapFailureSnapshot {
72    pub backend: KernelHeapBackend,
73    pub requested_size: usize,
74    pub align: usize,
75    pub effective_size: usize,
76    pub error: KernelHeapAllocError,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub struct SlabDiagSnapshot {
81    pub pages_allocated: usize,
82    pub pages_reclaimed: usize,
83    pub pages_live: usize,
84}
85
86#[inline]
87pub(crate) fn classify_kernel_heap_backend(layout: Layout) -> KernelHeapBackend {
88    let effective = layout.size().max(layout.align());
89    if effective <= MAX_SLAB_SIZE {
90        KernelHeapBackend::Slab
91    } else {
92        KernelHeapBackend::Vmalloc
93    }
94}
95
96// =============================================================================
97// CRITICAL: slab corruption detection
98//
99// Set HEAP_POISON_ENABLED to true during debugging of heap-corruption crashes.
100// When enabled:
101//   - Every block carved by refill() is filled with POISON_BYTE in bytes [8..N-4]
102//     and stamped with SLAB_CANARY in the last 4 bytes.
103//   - dealloc_block() restores the canary and re-poisons before linking.
104//   - alloc_block() verifies poison and canary before handing the block out;
105//     a mismatch is logged immediately via serial_println! (non-allocating).
106//
107// This detects:
108//   - Use-after-free: a write to a freed slab block overwrites poison bytes.
109//   - Buffer overflow: a write past the end overwrites the canary or the next
110//     block's free-list pointer.
111//
112// Cost: one memset + canary write per alloc/dealloc for slab classes.
113// =============================================================================
114const HEAP_POISON_ENABLED: bool = true;
115/// Byte pattern written to the body of freed slab blocks.
116const POISON_BYTE: u8 = 0xDE;
117/// Canary word placed at the last 4 bytes of each slab block.
118const SLAB_CANARY: u32 = 0xDEAD_BEEF;
119
120// ---------------------------------------------------------------------------
121// Slab page header : embedded at byte 0 of every buddy page used by a class.
122// Blocks start at offset SLAB_HEADER_SIZE within the page.
123// ---------------------------------------------------------------------------
124
125/// Header at the base of each 4 KiB page dedicated to a slab class.
126///
127/// Page layout:
128/// ```text
129/// [0 .. SLAB_HEADER_SIZE)   SlabPageHeader  (24 bytes)
130/// [SLAB_HEADER_SIZE .. 4096) slab blocks, each SLAB_SIZES[ci] bytes
131/// ```
132///
133/// A page lives in `partial_pages[ci]` while `0 < free_count < total_blocks`.
134/// It is removed when all blocks are allocated (`free_count == 0`), and is
135/// reclaimed to the buddy allocator when it becomes fully empty again
136/// (`free_count == total_blocks`).
137#[repr(C)]
138struct SlabPageHeader {
139    /// Next page in the partial list for this class (null = end of list).
140    next_partial: *mut SlabPageHeader,
141    /// Head of the intra-page free-block chain (null = page is full).
142    free_head: *mut u8,
143    /// Free blocks currently in this page.
144    free_count: u32,
145    /// Total blocks this page can hold (constant per class after refill).
146    total_blocks: u32,
147}
148
149// SAFETY: only accessed under SLAB_ALLOC spinlock.
150unsafe impl Send for SlabPageHeader {}
151unsafe impl Sync for SlabPageHeader {}
152
153/// Byte offset at which slab blocks begin within each slab page.
154const SLAB_HEADER_SIZE: usize = core::mem::size_of::<SlabPageHeader>();
155
156// Compile-time invariants.
157const _: () = assert!(
158    SLAB_HEADER_SIZE == 24,
159    "SlabPageHeader size changed : update docs"
160);
161const _: () = assert!(
162    (4096 - SLAB_HEADER_SIZE) / SLAB_SIZES[NUM_SLABS - 1] >= 1,
163    "SlabPageHeader too large: largest slab class gets 0 blocks per page"
164);
165
166// ---------------------------------------------------------------------------
167// SlabState
168// ---------------------------------------------------------------------------
169
170/// Per-size-class partial-page lists.
171///
172/// `partial_pages[ci]` is the head of a singly-linked list of `SlabPageHeader`
173/// nodes for class `ci`.  A page enters the list on `refill` and on the first
174/// `dealloc` after going full.  It leaves the list when all its blocks are
175/// allocated (it silently becomes "full") or when it becomes completely empty
176/// (it is then returned to the buddy allocator).
177struct SlabState {
178    partial_pages: [*mut SlabPageHeader; NUM_SLABS],
179}
180
181// SAFETY: protected exclusively through `SLAB_ALLOC: SpinLock<SlabState>`.
182unsafe impl Send for SlabState {}
183unsafe impl Sync for SlabState {}
184
185impl SlabState {
186    const fn new() -> Self {
187        SlabState {
188            partial_pages: [ptr::null_mut(); NUM_SLABS],
189        }
190    }
191
192    /// Return the slab class index for `layout`.
193    ///
194    /// The chosen class must be large enough for the payload and guarantee the
195    /// requested alignment for every block carved from that class.
196    #[inline]
197    fn class_index_for_layout(layout: Layout) -> usize {
198        for (i, &s) in SLAB_SIZES.iter().enumerate() {
199            if layout.size() <= s && layout.align() <= slab_class_alignment(i) {
200                return i;
201            }
202        }
203        unreachable!("class_index_for_layout called for unsupported slab layout")
204    }
205
206    /// Allocate one buddy page, write a `SlabPageHeader` at its base, carve
207    /// the remaining space into blocks, and prepend the page to `partial_pages[ci]`.
208    unsafe fn refill(&mut self, ci: usize, token: &crate::sync::IrqDisabledToken) {
209        debug_assert!(
210            !crate::arch::x86_64::interrupts_enabled(),
211            "refill: IRQs must be disabled (IrqDisabledToken contract)"
212        );
213        let slab_size = SLAB_SIZES[ci];
214        let slab_align = slab_class_alignment(ci);
215        let blocks_offset = (SLAB_HEADER_SIZE + slab_align - 1) & !(slab_align - 1);
216        let num_blocks = (4096 - blocks_offset) / slab_size;
217        debug_assert!(
218            num_blocks >= 1,
219            "refill: slab_size {} yields 0 blocks",
220            slab_size
221        );
222
223        let frame = match memory::allocate_frame(token) {
224            Ok(f) => f,
225            Err(_) => return, // OOM : alloc_block will see null partial and return null
226        };
227        SLAB_PAGES_ALLOCATED.fetch_add(1, AtomicOrdering::Relaxed);
228
229        let page_virt = super::phys_to_virt(frame.start_address.as_u64()) as *mut u8;
230
231        // Initialise page header at byte 0.
232        let header = page_virt as *mut SlabPageHeader;
233        (*header).next_partial = ptr::null_mut();
234        (*header).free_head = ptr::null_mut();
235        (*header).free_count = 0;
236        (*header).total_blocks = num_blocks as u32;
237
238        // Carve blocks starting at an alignment-respecting offset, highest index first so
239        // the lowest-address block ends up at the head (cosmetic only).
240        let blocks_start = page_virt.add(blocks_offset);
241        for i in (0..num_blocks).rev() {
242            let block = blocks_start.add(i * slab_size);
243            debug_assert_eq!(
244                (block as usize) & (slab_align - 1),
245                0,
246                "slab block alignment invariant broken for class {}",
247                slab_size
248            );
249            *(block as *mut *mut u8) = (*header).free_head;
250            if HEAP_POISON_ENABLED {
251                let end = slab_size.saturating_sub(4);
252                for off in 8..end {
253                    *block.add(off) = POISON_BYTE;
254                }
255                if slab_size >= 12 {
256                    let cp = block.add(slab_size - 4) as *mut u32;
257                    *cp = SLAB_CANARY;
258                }
259            }
260            (*header).free_head = block;
261            (*header).free_count += 1;
262        }
263
264        // Prepend to partial list.
265        (*header).next_partial = self.partial_pages[ci];
266        self.partial_pages[ci] = header;
267    }
268
269    /// Pop one block from the first partial page for class `ci`.
270    /// Calls `refill` when the partial list is empty.  Returns null on OOM.
271    unsafe fn alloc_block(&mut self, ci: usize, token: &crate::sync::IrqDisabledToken) -> *mut u8 {
272        if self.partial_pages[ci].is_null() {
273            self.refill(ci, token);
274        }
275        let header = self.partial_pages[ci];
276        if header.is_null() {
277            return ptr::null_mut();
278        }
279
280        let block = (*header).free_head;
281        debug_assert!(
282            !block.is_null(),
283            "alloc_block: partial page has null free_head"
284        );
285
286        (*header).free_head = *(block as *const *mut u8);
287        (*header).free_count -= 1;
288
289        // Remove page from partial list when it is now full (free_count == 0).
290        if (*header).free_count == 0 {
291            self.partial_pages[ci] = (*header).next_partial;
292            (*header).next_partial = ptr::null_mut();
293        }
294
295        if HEAP_POISON_ENABLED {
296            let slab_size = SLAB_SIZES[ci];
297            let end = slab_size.saturating_sub(4);
298            let mut bad_off: Option<usize> = None;
299            for off in 8..end {
300                if *block.add(off) != POISON_BYTE {
301                    bad_off = Some(off);
302                    break;
303                }
304            }
305            if let Some(off) = bad_off {
306                let b0 = *block.add(off);
307                let b1 = if off + 1 < slab_size {
308                    *block.add(off + 1)
309                } else {
310                    0
311                };
312                let b2 = if off + 2 < slab_size {
313                    *block.add(off + 2)
314                } else {
315                    0
316                };
317                let b3 = if off + 3 < slab_size {
318                    *block.add(off + 3)
319                } else {
320                    0
321                };
322                crate::serial_println!(
323                    "\x1b[1;31m[HEAP] USE-AFTER-FREE: slab[{}] block={:#x} off={} bytes=[{:02x} {:02x} {:02x} {:02x}]\x1b[0m",
324                    slab_size,
325                    block as u64,
326                    off,
327                    b0,
328                    b1,
329                    b2,
330                    b3
331                );
332            }
333            if slab_size >= 12 {
334                let canary = *(block.add(slab_size - 4) as *const u32);
335                if canary != SLAB_CANARY {
336                    crate::serial_println!(
337                        "\x1b[1;31m[HEAP] CANARY OVERFLOW: slab[{}] block={:#x} expected={:#x} got={:#x}\x1b[0m",
338                        slab_size,
339                        block as u64,
340                        SLAB_CANARY,
341                        canary
342                    );
343                }
344            }
345        }
346
347        block
348    }
349
350    /// Return `ptr` to its slab page and reclaim the page to the buddy
351    /// allocator if it becomes fully empty.
352    unsafe fn dealloc_block(
353        &mut self,
354        ptr: *mut u8,
355        ci: usize,
356        token: &crate::sync::IrqDisabledToken,
357    ) {
358        let slab_size = SLAB_SIZES[ci];
359
360        if HEAP_POISON_ENABLED {
361            if slab_size >= 12 {
362                let cp = ptr.add(slab_size - 4) as *mut u32;
363                *cp = SLAB_CANARY;
364            }
365            let end = slab_size.saturating_sub(4);
366            for off in 8..end {
367                *ptr.add(off) = POISON_BYTE;
368            }
369        }
370
371        // Locate the page header: round ptr down to 4 KiB boundary.
372        let page_base = (ptr as usize) & !0xFFF;
373        let header = page_base as *mut SlabPageHeader;
374
375        let was_full = (*header).free_count == 0;
376
377        // Push block onto the page's intra-page free list.
378        *(ptr as *mut *mut u8) = (*header).free_head;
379        (*header).free_head = ptr;
380        (*header).free_count += 1;
381
382        if was_full {
383            // Page went full -> partial: re-insert at list head.
384            (*header).next_partial = self.partial_pages[ci];
385            self.partial_pages[ci] = header;
386        }
387
388        // Reclaim fully-empty pages to the buddy allocator.
389        if (*header).free_count == (*header).total_blocks {
390            self.remove_from_partial(header, ci);
391            let phys = super::virt_to_phys(page_base as u64);
392            // Zero the header before freeing to catch accidental reuse.
393            core::ptr::write_bytes(header as *mut u8, 0, SLAB_HEADER_SIZE);
394            let frame = memory::frame::PhysFrame {
395                start_address: PhysAddr::new(phys),
396            };
397            memory::free_frame(token, frame);
398            SLAB_PAGES_RECLAIMED.fetch_add(1, AtomicOrdering::Relaxed);
399        }
400    }
401
402    /// Unlink `page` from `partial_pages[ci]`.  O(n) in partial-list length.
403    unsafe fn remove_from_partial(&mut self, page: *mut SlabPageHeader, ci: usize) {
404        if self.partial_pages[ci] == page {
405            self.partial_pages[ci] = (*page).next_partial;
406            (*page).next_partial = ptr::null_mut();
407            return;
408        }
409        let mut cur = self.partial_pages[ci];
410        while !cur.is_null() {
411            let next = (*cur).next_partial;
412            if next == page {
413                (*cur).next_partial = (*page).next_partial;
414                (*page).next_partial = ptr::null_mut();
415                return;
416            }
417            cur = next;
418        }
419        debug_assert!(
420            false,
421            "remove_from_partial: page {:p} not found in class {} list",
422            page, ci
423        );
424    }
425}
426
427static SLAB_ALLOC: SpinLock<SlabState> = SpinLock::new(SlabState::new());
428static LAST_HEAP_FAILURE: SpinLock<Option<KernelHeapFailureSnapshot>> = SpinLock::new(None);
429
430/// Total buddy pages ever handed to the slab allocator.
431static SLAB_PAGES_ALLOCATED: AtomicUsize = AtomicUsize::new(0);
432/// Total buddy pages ever returned from the slab allocator (fully-empty reclaim).
433static SLAB_PAGES_RECLAIMED: AtomicUsize = AtomicUsize::new(0);
434
435/// Returns the slab lock address for deadlock tracing.
436pub fn debug_slab_lock_addr() -> usize {
437    &SLAB_ALLOC as *const _ as usize
438}
439
440/// Register slab lock for E9 trace (call from init).
441pub fn debug_register_slab_trace() {
442    crate::sync::debug_set_trace_slab_addr(debug_slab_lock_addr());
443}
444
445// ---------------------------------------------------------------------------
446// GlobalAlloc implementation
447// ---------------------------------------------------------------------------
448
449pub struct LockedHeap;
450
451unsafe impl GlobalAlloc for LockedHeap {
452    /// Performs the alloc operation.
453    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
454        try_alloc_kernel_heap(layout).unwrap_or(ptr::null_mut())
455    }
456
457    /// Performs the dealloc operation.
458    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
459        let effective = layout.size().max(layout.align());
460
461        match classify_kernel_heap_backend(layout) {
462            KernelHeapBackend::Slab => {
463                // --- slab path: return block to free list ---
464                let ci = SlabState::class_index_for_layout(layout);
465                let cpu = crate::arch::x86_64::percpu::current_cpu_index();
466                let irq_enabled = crate::arch::x86_64::interrupts_enabled();
467                #[cfg(debug_assertions)]
468                if !irq_enabled {
469                    use core::sync::atomic::{AtomicUsize, Ordering};
470                    static HEAP_D_COUNT: AtomicUsize = AtomicUsize::new(0);
471                    let n = HEAP_D_COUNT.fetch_add(1, Ordering::Relaxed);
472                    if n % 100 == 0 {
473                        crate::e9_println!(
474                            "HEAP-D cpu={} irq=0 size={} ci={} n={}",
475                            cpu,
476                            effective,
477                            ci,
478                            n
479                        );
480                    }
481                }
482                // Catch layout mismatches where a vmalloc pointer is freed with
483                // a small layout (classify_kernel_heap_backend routes to Slab).
484                // This means the caller passed a different layout to dealloc than
485                // was used for alloc : a GlobalAlloc contract violation.
486                #[cfg(debug_assertions)]
487                {
488                    let addr = ptr as u64;
489                    if addr >= crate::memory::vmalloc::VMALLOC_VIRT_START
490                        && addr < crate::memory::vmalloc::VMALLOC_VIRT_END
491                    {
492                        crate::serial_println!(
493                            "[heap][bug] slab dealloc: ptr {:#x} is in vmalloc range : layout mismatch",
494                            addr
495                        );
496                        debug_assert!(
497                            false,
498                            "slab dealloc with vmalloc pointer : alloc/dealloc layout mismatch"
499                        );
500                    }
501                }
502                let mut slab = SLAB_ALLOC.lock();
503                slab.with_mut_and_token(|s, token| s.dealloc_block(ptr, ci, token));
504            }
505            KernelHeapBackend::Vmalloc => {
506                // vmalloc path: free via the vmalloc arena
507                let addr = ptr as u64;
508                if addr >= crate::memory::vmalloc::VMALLOC_VIRT_START
509                    && addr < crate::memory::vmalloc::VMALLOC_VIRT_END
510                {
511                    let ok = crate::sync::with_irqs_disabled(|token| {
512                        crate::memory::free_kernel_virtual(ptr, token)
513                    });
514                    if !ok {
515                        crate::serial_println!(
516                            "[heap][leak] vmalloc free: no live mapping at {:#x} (wrong base or double-free?)",
517                            addr
518                        );
519                    }
520                } else {
521                    // Pointer is outside the vmalloc arena with a large-allocation
522                    // layout : nothing is freed (GlobalAlloc contract violation).
523                    crate::serial_println!(
524                        "[heap][leak] vmalloc dealloc: ptr {:#x} outside vmalloc arena [{:#x}..{:#x}]",
525                        addr,
526                        crate::memory::vmalloc::VMALLOC_VIRT_START,
527                        crate::memory::vmalloc::VMALLOC_VIRT_END,
528                    );
529                    #[cfg(debug_assertions)]
530                    debug_assert!(
531                        false,
532                        "vmalloc dealloc with out-of-range pointer : memory leaked"
533                    );
534                }
535            }
536        }
537    }
538}
539
540fn record_heap_failure(
541    layout: Layout,
542    effective: usize,
543    backend: KernelHeapBackend,
544    error: KernelHeapAllocError,
545) -> KernelHeapAllocError {
546    *LAST_HEAP_FAILURE.lock() = Some(KernelHeapFailureSnapshot {
547        backend,
548        requested_size: layout.size(),
549        align: layout.align(),
550        effective_size: effective,
551        error,
552    });
553    error
554}
555
556pub fn last_heap_failure_snapshot() -> Option<KernelHeapFailureSnapshot> {
557    *LAST_HEAP_FAILURE.lock()
558}
559
560pub fn slab_diag_snapshot() -> SlabDiagSnapshot {
561    let allocated = SLAB_PAGES_ALLOCATED.load(AtomicOrdering::Relaxed);
562    let reclaimed = SLAB_PAGES_RECLAIMED.load(AtomicOrdering::Relaxed);
563    SlabDiagSnapshot {
564        pages_allocated: allocated,
565        pages_reclaimed: reclaimed,
566        pages_live: allocated.saturating_sub(reclaimed),
567    }
568}
569
570/// Number of slab size classes.
571pub const SLAB_NUM_CLASSES: usize = NUM_SLABS;
572
573/// Block size in bytes for slab class `ci`.
574///
575/// Panics in debug if `ci >= SLAB_NUM_CLASSES`.
576#[inline]
577pub fn slab_class_size(ci: usize) -> usize {
578    SLAB_SIZES[ci]
579}
580
581/// Guaranteed alignment in bytes for slab class `ci`.
582///
583/// This is the largest power-of-two divisor of the class size.
584#[inline]
585pub fn slab_class_alignment(ci: usize) -> usize {
586    1usize << SLAB_SIZES[ci].trailing_zeros()
587}
588
589/// Number of blocks that fit in one buddy page for slab class `ci`.
590///
591/// Accounts for the `SlabPageHeader` at the base of each page.
592#[inline]
593pub fn slab_blocks_per_page(ci: usize) -> usize {
594    let align = slab_class_alignment(ci);
595    let blocks_offset = (SLAB_HEADER_SIZE + align - 1) & !(align - 1);
596    (4096 - blocks_offset) / SLAB_SIZES[ci]
597}
598
599/// Returns whether the slab page at `page_base` is currently present in the
600/// partial-page list for class `ci`.
601///
602/// Returns `None` if the slab allocator lock is contended. Intended for
603/// shell/debug validation, not for allocator hot paths.
604pub fn slab_page_in_partial_list(ci: usize, page_base: u64) -> Option<bool> {
605    let mut guard = SLAB_ALLOC.try_lock()?;
606    Some(guard.with_mut_and_token(|s, _| unsafe {
607        let mut cur = s.partial_pages[ci];
608        while !cur.is_null() {
609            if cur as u64 == page_base {
610                return true;
611            }
612            cur = (*cur).next_partial;
613        }
614        false
615    }))
616}
617
618/// Fallible heap entry point with explicit backend-aware errors.
619///
620/// Kernel code that can recover from allocation failure should prefer this API
621/// over `Box`/`Vec`/`GlobalAlloc`, which eventually route to
622/// [`alloc_error_handler`] and remain fatal by language contract.
623#[inline]
624pub unsafe fn try_alloc_kernel_heap(layout: Layout) -> Result<*mut u8, KernelHeapAllocError> {
625    // Effective size must satisfy both the size and alignment requirements.
626    let effective = layout.size().max(layout.align());
627    // `Layout` constructors guarantee a non-zero alignment; keep the power-of-two
628    // check as a defensive guard for any malformed caller input.
629    if !layout.align().is_power_of_two() {
630        return Err(record_heap_failure(
631            layout,
632            effective,
633            classify_kernel_heap_backend(layout),
634            KernelHeapAllocError::InvalidLayout,
635        ));
636    }
637    // Large heap path uses vmalloc, which only aligns to 4 KiB pages.
638    if effective > MAX_SLAB_SIZE && layout.align() > 4096 {
639        return Err(record_heap_failure(
640            layout,
641            effective,
642            KernelHeapBackend::Vmalloc,
643            KernelHeapAllocError::AlignmentExceedsKernelPage {
644                align: layout.align(),
645            },
646        ));
647    }
648    let boot_reg = crate::silo::debug_boot_reg_active();
649    if boot_reg {
650        crate::serial_println!(
651            "[trace][heap] alloc enter effective={} size={} align={}",
652            effective,
653            layout.size(),
654            layout.align()
655        );
656    }
657
658    let result = match classify_kernel_heap_backend(layout) {
659        KernelHeapBackend::Slab => {
660            // --- slab path ---
661            let ci = SlabState::class_index_for_layout(layout);
662            // Race/corruption diagnostic: log alloc when IRQs disabled (rate-limited).
663            let cpu = crate::arch::x86_64::percpu::current_cpu_index();
664            let irq_enabled = crate::arch::x86_64::interrupts_enabled();
665            #[cfg(debug_assertions)]
666            if !irq_enabled {
667                use core::sync::atomic::{AtomicUsize, Ordering};
668                static HEAP_A_COUNT: AtomicUsize = AtomicUsize::new(0);
669                let n = HEAP_A_COUNT.fetch_add(1, Ordering::Relaxed);
670                if n % 100 == 0 {
671                    crate::e9_println!(
672                        "HEAP-A cpu={} irq=0 size={} ci={} n={}",
673                        cpu,
674                        effective,
675                        ci,
676                        n
677                    );
678                }
679            }
680            if boot_reg {
681                crate::serial_println!(
682                    "[trace][heap] alloc slab ci={} slab_size={} lock={:#x}",
683                    ci,
684                    SLAB_SIZES[ci],
685                    &SLAB_ALLOC as *const _ as usize
686                );
687            }
688            let mut slab = SLAB_ALLOC.lock();
689            if boot_reg {
690                crate::serial_println!("[trace][heap] alloc slab lock acquired");
691            }
692            let ptr = slab.with_mut_and_token(|s, token| s.alloc_block(ci, token));
693            if ptr.is_null() {
694                return Err(record_heap_failure(
695                    layout,
696                    effective,
697                    KernelHeapBackend::Slab,
698                    KernelHeapAllocError::SlabRefillFailed {
699                        effective,
700                        class_size: SLAB_SIZES[ci],
701                    },
702                ));
703            }
704            ptr
705        }
706        KernelHeapBackend::Vmalloc => {
707            // --- vmalloc path (large allocation) ---
708            if boot_reg {
709                crate::serial_println!("[trace][heap] alloc vmalloc size={}", effective);
710            }
711
712            crate::sync::with_irqs_disabled(|token| {
713                crate::memory::allocate_kernel_virtual(effective, token).map_err(|error| {
714                    record_heap_failure(
715                        layout,
716                        effective,
717                        KernelHeapBackend::Vmalloc,
718                        KernelHeapAllocError::Vmalloc(error),
719                    )
720                })
721            })?
722        }
723    };
724
725    Ok(result)
726}
727
728#[global_allocator]
729static HEAP_ALLOCATOR: LockedHeap = LockedHeap;
730
731/// Compatibility facade over the current global kernel heap policy.
732///
733/// Callers that need an explicit heap allocation entry point, rather than
734/// relying on `Box`/`Vec`/`GlobalAlloc`, should use this helper. The selected
735/// backend remains the current heap policy:
736/// - small allocations -> slab
737/// - large allocations -> vmalloc
738#[inline]
739pub unsafe fn alloc_kernel_heap(layout: Layout) -> *mut u8 {
740    try_alloc_kernel_heap(layout).unwrap_or(ptr::null_mut())
741}
742
743/// Free memory previously returned by [`alloc_kernel_heap`].
744#[inline]
745pub unsafe fn dealloc_kernel_heap(ptr: *mut u8, layout: Layout) {
746    HEAP_ALLOCATOR.dealloc(ptr, layout);
747}
748
749fn log_common_oom_header(layout: Layout, effective: usize) {
750    let cpu = crate::arch::x86_64::percpu::current_cpu_index();
751    let irq_enabled = crate::arch::x86_64::interrupts_enabled();
752    let tid = crate::process::current_task_id()
753        .map(|t| t.as_u64())
754        .unwrap_or(0);
755    let task_name = crate::process::current_task_clone()
756        .map(|t| t.name)
757        .unwrap_or("<none>");
758
759    crate::serial_println!(
760        "[heap][oom] cpu={} irq={} tid={} task={} size={} align={} effective={}",
761        cpu,
762        irq_enabled,
763        tid,
764        task_name,
765        layout.size(),
766        layout.align(),
767        effective
768    );
769}
770
771fn log_buddy_snapshot() -> Option<(usize, usize, usize)> {
772    if let Some(guard) = crate::memory::buddy::get_allocator().try_lock() {
773        if let Some(alloc) = guard.as_ref() {
774            let (total_pages, allocated_pages) = alloc.page_totals();
775            let free_pages = total_pages.saturating_sub(allocated_pages);
776            let fail_counts = crate::memory::buddy::buddy_alloc_fail_counts_snapshot();
777
778            crate::serial_println!(
779                "[heap][oom] buddy: total={} alloc={} free={}",
780                total_pages,
781                allocated_pages,
782                free_pages
783            );
784
785            let mut fail_line = alloc::string::String::from("[heap][oom] buddy_fail_by_order:");
786            for (i, &count) in fail_counts.iter().enumerate() {
787                use core::fmt::Write;
788                let _ = write!(fail_line, " o{}={} ", i, count);
789            }
790            crate::serial_println!("{}", fail_line);
791            return Some((total_pages, allocated_pages, free_pages));
792        }
793        crate::serial_println!("[heap][oom] buddy: allocator uninitialized");
794        return None;
795    }
796
797    crate::serial_println!("[heap][oom] buddy: allocator locked");
798    None
799}
800
801fn log_heap_failure_policy(layout: Layout) {
802    match last_heap_failure_snapshot() {
803        Some(snapshot) => {
804            crate::serial_println!(
805                "[heap][oom] last_failure backend={:?} requested={} align={} effective={} error={:?}",
806                snapshot.backend,
807                snapshot.requested_size,
808                snapshot.align,
809                snapshot.effective_size,
810                snapshot.error
811            );
812            if snapshot.requested_size != layout.size() || snapshot.align != layout.align() {
813                crate::serial_println!(
814                    "[heap][oom] note=last_heap_failure does not exactly match current layout; using best-effort context"
815                );
816            }
817        }
818        None => crate::serial_println!("[heap][oom] last_heap_failure unavailable"),
819    }
820}
821
822/// Allocates error handler.
823#[alloc_error_handler]
824fn alloc_error_handler(layout: Layout) -> ! {
825    let effective = layout.size().max(layout.align());
826    let pages_needed = (effective.saturating_add(4095)) / 4096;
827    let order = if pages_needed == 0 {
828        0
829    } else {
830        pages_needed.next_power_of_two().trailing_zeros() as u8
831    };
832    log_common_oom_header(layout, effective);
833
834    if effective <= MAX_SLAB_SIZE {
835        crate::serial_println!(
836            "[heap][oom] backend=slab effective={} class_max={} refill_order=0",
837            effective,
838            MAX_SLAB_SIZE
839        );
840        log_heap_failure_policy(layout);
841        if let Some((total_pages, _, free_pages)) = log_buddy_snapshot() {
842            crate::serial_println!(
843                "[heap][oom] slab-refill pages={} buddy_order={}",
844                pages_needed,
845                order
846            );
847            if free_pages > (total_pages / 4) {
848                crate::serial_println!(
849                    "[heap][oom] diagnosis=slab order-0 refill failed despite remaining free pages \
850                     ({} free pages): allocator pressure, zone exhaustion, or transient allocator state",
851                    free_pages,
852                );
853            }
854        }
855    } else {
856        crate::serial_println!(
857            "[heap][oom] backend=vmalloc request_pages={} legacy_buddy_order_hint={}",
858            pages_needed,
859            order
860        );
861        log_heap_failure_policy(layout);
862        if let Some(snap) = last_heap_failure_snapshot() {
863            if let KernelHeapAllocError::AlignmentExceedsKernelPage { align } = snap.error {
864                crate::serial_println!(
865                    "[heap][oom] diagnosis=layout alignment {} B exceeds 4 KiB page alignment guaranteed by vmalloc heap path",
866                    align
867                );
868            }
869        }
870        match crate::memory::vmalloc::last_failure_snapshot() {
871            Some(snapshot) => {
872                crate::serial_println!(
873                    "[heap][oom] vmalloc_last_failure size={} pages={} error={:?}",
874                    snapshot.size,
875                    snapshot.pages,
876                    snapshot.error
877                );
878                match snapshot.error {
879                    crate::memory::vmalloc::VmallocError::SizeExceedsPolicy {
880                        requested,
881                        max_allowed,
882                    } => {
883                        crate::serial_println!(
884                            "[heap][oom] diagnosis=vmalloc policy limit exceeded requested={} max_allowed={}",
885                            requested,
886                            max_allowed
887                        );
888                    }
889                    crate::memory::vmalloc::VmallocError::VirtualRangeExhausted => {
890                        crate::serial_println!(
891                            "[heap][oom] diagnosis=kernel virtual allocation arena exhausted or fragmented"
892                        );
893                    }
894                    crate::memory::vmalloc::VmallocError::PhysicalMemoryExhausted => {
895                        crate::serial_println!(
896                            "[heap][oom] diagnosis=vmalloc could not acquire enough physical pages"
897                        );
898                    }
899                    crate::memory::vmalloc::VmallocError::MetadataAllocationFailed => {
900                        crate::serial_println!(
901                            "[heap][oom] diagnosis=vmalloc metadata allocation failed"
902                        );
903                    }
904                    crate::memory::vmalloc::VmallocError::KernelMapFailed => {
905                        crate::serial_println!(
906                            "[heap][oom] diagnosis=kernel page-table mapping failed during vmalloc"
907                        );
908                    }
909                    crate::memory::vmalloc::VmallocError::ZeroSize => {
910                        crate::serial_println!("[heap][oom] diagnosis=zero-sized vmalloc request");
911                    }
912                }
913            }
914            None => {
915                crate::serial_println!("[heap][oom] vmalloc_last_failure unavailable");
916            }
917        }
918        let _ = log_buddy_snapshot();
919    }
920    crate::serial_println!(
921        "[heap][oom] policy=fatal_global_alloc_path use try_alloc_kernel_heap()/allocate_kernel_virtual() on recoverable paths"
922    );
923    panic!("fatal kernel heap allocation failure: {:?}", layout)
924}
925
926/// Dump heap and buddy allocator diagnostics to the serial console.
927///
928/// Safe to call from the shell or debug tooling. Prints:
929/// - Total/allocated/free pages
930/// - Per-order buddy free list head counts
931/// - Buddy allocation failure counts by order (fragmentation indicator)
932/// - Slab free list head pointers
933pub fn dump_diagnostics() {
934    crate::serial_println!("[heap][diag] === Heap Diagnostics ===");
935
936    // Buddy allocator stats
937    if let Some(guard) = crate::memory::buddy::get_allocator().try_lock() {
938        if let Some(alloc) = guard.as_ref() {
939            let (total_pages, allocated_pages) = alloc.page_totals();
940            let mut zones =
941                [crate::memory::buddy::ZoneStats::empty(); crate::memory::zone::ZoneType::COUNT];
942            let zone_count = alloc.zone_snapshot(&mut zones);
943            crate::serial_println!(
944                "[heap][diag] buddy: total={} pages, allocated={} pages, free={} pages",
945                total_pages,
946                allocated_pages,
947                total_pages.saturating_sub(allocated_pages)
948            );
949
950            for info in zones.iter().take(zone_count) {
951                crate::serial_println!(
952                    "[heap][diag] zone={:?} state={:?} managed={} present={} reserved={} free={} cached={} cu/cm={}/{} avail={} segments={}/{} pageblocks=u{}/m{} u_free={} m_free={} watermarks={}/{}/{} reserve={} largest_order={:?}",
953                    info.zone_type,
954                    info.pressure(),
955                    info.managed_pages,
956                    info.present_pages,
957                    info.reserved_pages,
958                    info.free_pages,
959                    info.cached_pages,
960                    info.cached_unmovable_pages,
961                    info.cached_movable_pages,
962                    info.available_after_reserve_pages(),
963                    info.segment_count,
964                    info.segment_capacity,
965                    info.unmovable_pageblocks,
966                    info.movable_pageblocks,
967                    info.unmovable_free_pages,
968                    info.movable_free_pages,
969                    info.watermark_min,
970                    info.watermark_low,
971                    info.watermark_high,
972                    info.lowmem_reserve_pages,
973                    info.largest_free_order
974                );
975            }
976
977            // Per-zone free list heads by migratetype.
978            for zi in 0..zone_count {
979                let zone = alloc.get_zone(zi);
980                let info = zones[zi];
981                let mut line = alloc::string::String::from("[heap][diag] ");
982                use core::fmt::Write;
983                let _ = write!(line, "zone={:?} free_heads:", zone.zone_type);
984                for order in 0..=crate::memory::zone::MAX_ORDER {
985                    let unmovable = zone.free_list_count_for(
986                        order as u8,
987                        crate::memory::zone::Migratetype::Unmovable,
988                    );
989                    let movable = zone.free_list_count_for(
990                        order as u8,
991                        crate::memory::zone::Migratetype::Movable,
992                    );
993                    if unmovable > 0 || movable > 0 {
994                        let _ = write!(line, " o{}=u{}/m{} ", order, unmovable, movable);
995                    }
996                }
997                crate::serial_println!("{}", line);
998
999                let mut frag = alloc::string::String::from("[heap][diag] ");
1000                let _ = write!(frag, "zone={:?} frag:", zone.zone_type);
1001                for order in 1..=crate::memory::zone::MAX_ORDER {
1002                    let score = zone.fragmentation_score(order as u8, info.cached_pages);
1003                    let _ = write!(frag, " o{}={}%", order, score);
1004                }
1005                crate::serial_println!("{}", frag);
1006            }
1007        }
1008    } else {
1009        crate::serial_println!("[heap][diag] buddy: allocator locked (retry later)");
1010    }
1011
1012    // Buddy failure counts
1013    let fail_counts = crate::memory::buddy::buddy_alloc_fail_counts_snapshot();
1014    let mut has_fails = false;
1015    for (i, &count) in fail_counts.iter().enumerate() {
1016        if count > 0 {
1017            has_fails = true;
1018        }
1019        crate::serial_println!("[heap][diag] buddy_fail[{}]: {}", i, count);
1020    }
1021    if has_fails {
1022        crate::serial_println!(
1023            "[heap][diag] => non-zero buddy_fail counts indicate fragmentation pressure"
1024        );
1025    }
1026
1027    // Slab stats
1028    {
1029        let alloc = SLAB_PAGES_ALLOCATED.load(AtomicOrdering::Relaxed);
1030        let reclaim = SLAB_PAGES_RECLAIMED.load(AtomicOrdering::Relaxed);
1031        crate::serial_println!(
1032            "[heap][diag] slab: pages_allocated={} pages_reclaimed={} pages_live={}",
1033            alloc,
1034            reclaim,
1035            alloc.saturating_sub(reclaim)
1036        );
1037    }
1038    if let Some(mut guard) = SLAB_ALLOC.try_lock() {
1039        // SAFETY: we hold the slab lock; raw pointer traversal is safe.
1040        guard.with_mut_and_token(|s, _| unsafe {
1041            for ci in 0..NUM_SLABS {
1042                let mut head = s.partial_pages[ci];
1043                if head.is_null() {
1044                    continue;
1045                }
1046                let mut page_count = 0usize;
1047                let mut free_blocks = 0u32;
1048                while !head.is_null() {
1049                    page_count += 1;
1050                    free_blocks = free_blocks.saturating_add((*head).free_count);
1051                    head = (*head).next_partial;
1052                }
1053                crate::serial_println!(
1054                    "[heap][diag] slab[{}]: partial_pages={} free_blocks={}",
1055                    SLAB_SIZES[ci],
1056                    page_count,
1057                    free_blocks
1058                );
1059            }
1060        });
1061    } else {
1062        crate::serial_println!("[heap][diag] slab: locked (retry later)");
1063    }
1064
1065    // Contiguous-physical allocation telemetry
1066    {
1067        let d = crate::memory::phys_contiguous_diag();
1068        crate::serial_println!(
1069            "[heap][diag] phys_contiguous: pages_allocated={} pages_freed={} pages_live={} alloc_failures={}",
1070            d.pages_allocated,
1071            d.pages_freed,
1072            d.pages_live,
1073            d.alloc_fail_count
1074        );
1075    }
1076
1077    if let Some(snapshot) = last_heap_failure_snapshot() {
1078        crate::serial_println!(
1079            "[heap][diag] last_heap_failure: backend={:?} requested={} align={} effective={} error={:?}",
1080            snapshot.backend,
1081            snapshot.requested_size,
1082            snapshot.align,
1083            snapshot.effective_size,
1084            snapshot.error
1085        );
1086    }
1087
1088    crate::memory::vmalloc::dump_diagnostics();
1089
1090    crate::serial_println!("[heap][diag] === End Diagnostics ===");
1091}