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}