⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/mm/
heap.rs

1//! Kernel heap allocator
2//!
3//! Implements a slab allocator for the kernel heap with size classes
4//! and per-CPU caches for performance.
5
6#![allow(dead_code, clippy::unwrap_or_default)]
7
8#[cfg(target_arch = "x86_64")]
9use core::{alloc::Layout, ptr::NonNull};
10
11#[cfg(target_arch = "x86_64")]
12use linked_list_allocator::LockedHeap;
13#[cfg(target_arch = "x86_64")]
14use spin::Mutex;
15
16#[cfg(target_arch = "x86_64")]
17use super::VirtualAddress;
18
19// Static heap storage - kept in BSS for layout stability across all
20// architectures. x86_64 uses this directly; AArch64/RISC-V use fixed physical
21// addresses instead to avoid BSS/heap overlap issues.
22//
23// SAFETY JUSTIFICATION: This static mut is intentionally kept because:
24// 1. It provides the raw backing memory for the kernel heap allocator
25// 2. It must exist before the heap is initialized (pre-heap bootstrap)
26// 3. Only accessed via addr_of_mut!() in init(), never through &mut references
27// 4. After init, all access goes through the allocator's own synchronization
28// 5. Cannot use OnceLock/GlobalState as those require heap allocation
29//    themselves
30// x86_64 uses a 512MB heap to support loading the self-hosting toolchain
31// rootfs (~57MB TAR with BusyBox source + GCC toolchain) and Phase C native
32// compilation. The rootfs extracts to ~54MB of VFS content (cc1 alone is
33// 35MB). During exec, fs::read_file() creates a second 35MB copy of cc1
34// for ELF loading. BlockFS persistent storage uses ~128MB for in-memory
35// block cache. Combined with VFS metadata (~10MB), process structures,
36// and heap fragmentation from Phase B tests, 384MB was insufficient when
37// both BlockFS and native compilation are active.
38// 1GB provides headroom for BlockFS cache, VFS, native compilation, and
39// the Rust toolchain rootfs (rustc+cargo+std ~400MB). Requires QEMU -m 4096M
40// minimum (typically -m 32768M for Phase 6.5 self-hosting).
41// AArch64/RISC-V keep 8MB since they have less RAM (128MB default) and
42// don't use virtio-blk for rootfs loading.
43#[cfg(target_arch = "x86_64")]
44#[allow(static_mut_refs)]
45static mut HEAP_MEMORY: [u8; 1024 * 1024 * 1024] = [0; 1024 * 1024 * 1024];
46
47#[cfg(not(target_arch = "x86_64"))]
48#[allow(static_mut_refs)]
49static mut HEAP_MEMORY: [u8; 8 * 1024 * 1024] = [0; 8 * 1024 * 1024];
50
51/// Kernel heap size
52#[cfg(target_arch = "x86_64")]
53pub const HEAP_SIZE: usize = 1024 * 1024 * 1024;
54
55#[cfg(not(target_arch = "x86_64"))]
56pub const HEAP_SIZE: usize = 8 * 1024 * 1024;
57
58/// Kernel heap start address (re-exported from architecture module)
59pub const HEAP_START: usize = crate::arch::HEAP_START;
60
61/// Return the virtual address of the last byte of the HEAP_MEMORY array.
62///
63/// Used by the memory management init code when `__kernel_end` translation
64/// fails. The heap is the largest BSS object; its end address is a tight
65/// lower bound on the kernel's physical extent.
66///
67/// # Safety
68///
69/// Accesses the static `HEAP_MEMORY` address. This is safe because we only
70/// compute the pointer value -- we do not read from or write to it.
71pub fn heap_end_vaddr() -> u64 {
72    // SAFETY: We only compute the address of the end of HEAP_MEMORY.
73    // `addr_of!` avoids creating a reference to the `static mut`, which
74    // would be UB under Rust 2024 rules. The pointer arithmetic is valid
75    // as long as HEAP_MEMORY has been placed by the linker.
76    unsafe { (core::ptr::addr_of!(HEAP_MEMORY) as *const u8).add(HEAP_SIZE) as u64 }
77}
78
79/// Get current heap statistics (x86_64 only).
80///
81/// Returns (total, used, free) in bytes.
82#[cfg(all(target_arch = "x86_64", target_os = "none"))]
83pub fn get_heap_stats() -> (usize, usize, usize) {
84    let allocator = crate::get_allocator().lock();
85    let total = HEAP_SIZE;
86    let free = allocator.free();
87    let used = total.saturating_sub(free);
88    (total, used, free)
89}
90
91/// Slab allocator for efficient small allocations (x86_64 only, uses
92/// LockedHeap)
93#[cfg(target_arch = "x86_64")]
94pub struct SlabAllocator {
95    /// Size classes for slab allocation
96    slabs: [Option<Slab>; 10],
97    /// Fallback allocator for large allocations
98    fallback: LockedHeap,
99    /// Statistics
100    stats: Mutex<HeapStats>,
101}
102
103#[cfg(target_arch = "x86_64")]
104/// A slab for a specific size class
105struct Slab {
106    /// Object size for this slab
107    object_size: usize,
108    /// Free list head
109    free_list: Option<NonNull<FreeObject>>,
110    /// Number of free objects
111    free_count: usize,
112    /// Total objects in slab
113    total_objects: usize,
114    /// Base address of slab
115    base: VirtualAddress,
116}
117
118#[cfg(target_arch = "x86_64")]
119/// Free object in slab free list
120struct FreeObject {
121    next: Option<NonNull<FreeObject>>,
122}
123
124#[cfg(target_arch = "x86_64")]
125/// Heap statistics
126#[derive(Debug, Default, Clone)]
127pub struct HeapStats {
128    /// Total bytes allocated
129    pub allocated_bytes: usize,
130    /// Total bytes freed
131    pub freed_bytes: usize,
132    /// Current bytes in use
133    pub used_bytes: usize,
134    /// Peak bytes used
135    pub peak_bytes: usize,
136    /// Number of allocations
137    pub allocation_count: u64,
138    /// Number of frees
139    pub free_count: u64,
140}
141
142#[cfg(target_arch = "x86_64")]
143/// Size classes for slab allocator (in bytes)
144const SIZE_CLASSES: [usize; 10] = [8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
145
146#[cfg(target_arch = "x86_64")]
147impl Default for SlabAllocator {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153#[cfg(target_arch = "x86_64")]
154impl SlabAllocator {
155    /// Create a new slab allocator
156    pub const fn new() -> Self {
157        Self {
158            slabs: [const { None }; 10],
159            fallback: LockedHeap::empty(),
160            stats: Mutex::new(HeapStats {
161                allocated_bytes: 0,
162                freed_bytes: 0,
163                used_bytes: 0,
164                peak_bytes: 0,
165                allocation_count: 0,
166                free_count: 0,
167            }),
168        }
169    }
170
171    /// Initialize the slab allocator with heap memory
172    ///
173    /// # Safety
174    ///
175    /// The caller must ensure that:
176    /// - The heap_start address is valid and properly aligned
177    /// - The heap_size bytes starting at heap_start are available for use
178    /// - This function is called only once
179    pub unsafe fn init(&self, heap_start: usize, heap_size: usize) {
180        // Reserve some memory for slab metadata
181        let metadata_size = heap_size / 16; // 1/16 of heap for metadata
182        let slab_area_size = heap_size - metadata_size;
183
184        // Initialize fallback allocator with metadata area
185        self.fallback
186            .lock()
187            .init(heap_start as *mut u8, metadata_size);
188
189        // Initialize slabs
190        let mut current_addr = heap_start + metadata_size;
191        let slab_size = slab_area_size / SIZE_CLASSES.len();
192
193        for &size in SIZE_CLASSES.iter() {
194            if current_addr + slab_size > heap_start + heap_size {
195                break;
196            }
197
198            // Create slab for this size class
199            let _slab = self.init_slab(VirtualAddress::new(current_addr as u64), slab_size, size);
200
201            // Store in array (this is a bit tricky without mut self)
202            // In real implementation, would need interior mutability
203            // For now, this is a placeholder
204
205            current_addr += slab_size;
206        }
207    }
208
209    /// Initialize a single slab
210    fn init_slab(&self, base: VirtualAddress, size: usize, object_size: usize) -> Slab {
211        let objects_per_slab = size / object_size;
212        let mut free_list = None;
213
214        // Build free list
215        for i in (0..objects_per_slab).rev() {
216            let obj_addr = base.as_u64() + (i * object_size) as u64;
217            let obj_ptr = obj_addr as *mut FreeObject;
218
219            // SAFETY: `obj_ptr` is derived from `base` (the slab's base virtual address)
220            // plus a bounded offset `i * object_size` where `i < objects_per_slab` and
221            // `objects_per_slab = size / object_size`, so the pointer stays within the
222            // slab's allocated memory region [base, base + size). The slab memory was
223            // reserved during `init()` and is exclusively owned by this allocator.
224            // `obj_ptr` is non-null because `base` is a valid non-zero virtual address
225            // and the offset is bounded, so `NonNull::new_unchecked` is sound.
226            unsafe {
227                (*obj_ptr).next = free_list;
228                free_list = Some(NonNull::new_unchecked(obj_ptr));
229            }
230        }
231
232        Slab {
233            object_size,
234            free_list,
235            free_count: objects_per_slab,
236            total_objects: objects_per_slab,
237            base,
238        }
239    }
240
241    /// Allocate from appropriate slab
242    fn allocate(&self, layout: Layout) -> *mut u8 {
243        let size = layout.size().max(layout.align());
244
245        // Find appropriate size class
246        for &class_size in SIZE_CLASSES.iter() {
247            if size <= class_size {
248                // Try to allocate from this slab
249                // In real implementation, would need proper synchronization
250                // This is a placeholder
251                return self
252                    .fallback
253                    .lock()
254                    .allocate_first_fit(layout)
255                    .map(|ptr| ptr.as_ptr())
256                    .unwrap_or(core::ptr::null_mut());
257            }
258        }
259
260        // Large allocation - use fallback
261        self.fallback
262            .lock()
263            .allocate_first_fit(layout)
264            .map(|ptr| ptr.as_ptr())
265            .unwrap_or(core::ptr::null_mut())
266    }
267
268    /// Free to appropriate slab
269    fn deallocate(&self, ptr: *mut u8, layout: Layout) {
270        let size = layout.size().max(layout.align());
271
272        // Find appropriate size class
273        for &class_size in SIZE_CLASSES.iter() {
274            if size <= class_size {
275                // Return to slab
276                // In real implementation, would need proper synchronization
277                // SAFETY: `ptr` was returned by a prior `allocate_first_fit` call on the
278                // same `fallback` allocator, so it is a valid, non-null, properly aligned
279                // pointer to an allocation of the given `layout`. The caller guarantees
280                // the pointer is no longer in use (standard dealloc contract).
281                // `NonNull::new_unchecked` is sound because `ptr` originated from
282                // `allocate_first_fit` which only returns non-null pointers.
283                unsafe {
284                    self.fallback
285                        .lock()
286                        .deallocate(NonNull::new_unchecked(ptr), layout);
287                }
288                return;
289            }
290        }
291
292        // Large allocation - use fallback
293        // SAFETY: Same invariants as above -- `ptr` was returned by a prior
294        // `allocate_first_fit` call for this `layout`, so it is valid, non-null,
295        // and properly aligned. The caller guarantees exclusive ownership has been
296        // relinquished.
297        unsafe {
298            self.fallback
299                .lock()
300                .deallocate(NonNull::new_unchecked(ptr), layout);
301        }
302    }
303
304    /// Get heap statistics
305    pub fn stats(&self) -> HeapStats {
306        self.stats.lock().clone()
307    }
308}
309
310// Global slab allocator instance (not used for now)
311// static SLAB_ALLOCATOR: SlabAllocator = SlabAllocator::new();
312
313/// Initialize the kernel heap
314pub fn init() -> Result<(), crate::error::KernelError> {
315    kprintln!("[HEAP] Initializing kernel heap");
316
317    // SAFETY: We access `HEAP_MEMORY`, a static mut byte array in the kernel's BSS
318    // section. This function is called exactly once during kernel initialization
319    // (single-threaded boot context), so there are no concurrent accesses. The
320    // resulting `heap_start` pointer is valid for `heap_size` bytes and the memory
321    // does not overlap with any other allocation because it is a dedicated static
322    // array in the kernel binary.
323    #[allow(unused_unsafe, unused_variables)]
324    unsafe {
325        let heap_start = core::ptr::addr_of_mut!(HEAP_MEMORY) as *mut u8;
326        let heap_size = HEAP_SIZE;
327
328        // RISC-V: Use UnsafeBumpAllocator (same as AArch64).
329        // LockedHeap's linked-list free list gets corrupted on RISC-V bare
330        // metal ("Hole list out of order?"), so we use the simpler bump
331        // allocator with a 4MB heap that provides ample space for boot.
332        #[cfg(target_arch = "riscv64")]
333        {
334            println!("[HEAP] Initializing RISC-V UnsafeBumpAllocator");
335            println!(
336                "[HEAP] Heap start: {:p}, size: {} bytes",
337                heap_start, heap_size
338            );
339
340            // SAFETY: `ALLOCATOR` is the global bump allocator. `heap_start` points to
341            // valid memory of at least `heap_size` bytes (the static HEAP_MEMORY array).
342            // This is called once during single-threaded boot, so no concurrent access.
343            unsafe {
344                crate::ALLOCATOR.init(heap_start, heap_size);
345            }
346
347            // Initialize locked allocator for compatibility
348            {
349                let mut allocator = crate::get_allocator().lock();
350                // SAFETY: Same memory region, called once during boot.
351                unsafe {
352                    allocator.init(heap_start, heap_size);
353                }
354            }
355            println!("[HEAP] RISC-V heap initialization complete");
356        }
357
358        // AArch64: Use lock-free UnsafeBumpAllocator (LockedHeap deadlocks on AArch64)
359        // Initialize fields directly to avoid function call issues on AArch64
360        #[cfg(target_arch = "aarch64")]
361        {
362            use core::sync::atomic::Ordering;
363
364            use crate::arch::aarch64::direct_uart::uart_write_str;
365
366            uart_write_str("[HEAP] Initializing AArch64 UnsafeBumpAllocator\n");
367
368            let start_addr = heap_start as usize;
369
370            // Initialize ALLOCATOR atomics directly (bypasses function call)
371            crate::ALLOCATOR.start.store(start_addr, Ordering::SeqCst);
372            crate::ALLOCATOR.size.store(heap_size, Ordering::SeqCst);
373            crate::ALLOCATOR.next.store(start_addr, Ordering::SeqCst);
374            crate::ALLOCATOR.allocations.store(0, Ordering::SeqCst);
375            core::sync::atomic::fence(Ordering::SeqCst);
376
377            // AArch64 memory barriers
378            // SAFETY: DSB SY (Data Synchronization Barrier) and ISB (Instruction
379            // Synchronization Barrier) are architectural barrier instructions that
380            // are always safe to execute at any exception level. They ensure all
381            // preceding memory operations complete before subsequent ones begin,
382            // which is required after writing to the allocator's atomic fields so
383            // that the allocator state is visible before any allocation attempts.
384            unsafe {
385                core::arch::asm!("dsb sy", "isb", options(nomem, nostack));
386            }
387
388            uart_write_str("[HEAP] ALLOCATOR initialized\n");
389
390            // Verify allocator state
391            let next_val = crate::ALLOCATOR.next.load(Ordering::SeqCst);
392            if next_val != 0 {
393                uart_write_str("[HEAP] Allocator state verified OK\n");
394            } else {
395                uart_write_str("[HEAP] WARNING: Allocator next=0\n");
396            }
397
398            // Initialize locked allocator too (direct field access)
399            crate::LOCKED_ALLOCATOR
400                .inner
401                .start
402                .store(start_addr, Ordering::SeqCst);
403            crate::LOCKED_ALLOCATOR
404                .inner
405                .size
406                .store(heap_size, Ordering::SeqCst);
407            crate::LOCKED_ALLOCATOR
408                .inner
409                .next
410                .store(start_addr, Ordering::SeqCst);
411            crate::LOCKED_ALLOCATOR
412                .inner
413                .allocations
414                .store(0, Ordering::SeqCst);
415            core::sync::atomic::fence(Ordering::SeqCst);
416
417            uart_write_str("[HEAP] AArch64 heap initialization complete\n");
418        }
419
420        // x86_64: Use LockedHeap
421        // Note: The `init` call on LockedHeap is unsafe because it trusts the
422        // caller to provide valid memory. The outer unsafe block already
423        // establishes that `heap_start`/`heap_size` describe valid, exclusive
424        // memory from the static HEAP_MEMORY array.
425        #[cfg(all(target_arch = "x86_64", target_os = "none"))]
426        {
427            let mut allocator = crate::get_allocator().lock();
428            allocator.init(heap_start, heap_size);
429            drop(allocator);
430        }
431
432        println!(
433            "[HEAP] Heap initialized: {} KB at {:p}",
434            heap_size / 1024,
435            core::ptr::addr_of!(HEAP_MEMORY)
436        );
437    }
438
439    Ok(())
440}
441
442#[cfg(all(test, not(target_os = "none")))]
443mod tests {
444    use alloc::{boxed::Box, vec::Vec};
445
446    use super::*;
447
448    #[test]
449    fn test_heap_allocation() {
450        // Test various allocations
451        let x = Box::new(42);
452        assert_eq!(*x, 42);
453
454        let mut v = Vec::new();
455        for i in 0..100 {
456            v.push(i);
457        }
458        assert_eq!(v.len(), 100);
459    }
460
461    #[test]
462    fn test_size_classes() {
463        // Test that size classes are powers of 2 or nice round numbers
464        for &size in &SIZE_CLASSES {
465            assert!(size >= 8);
466            assert!(size <= 4096);
467        }
468    }
469}