The Kernel Heap

July 16, 2026 ยท View on GitHub

The kernel heap backs alloc, so Vec, Box, BTreeMap, and every other dynamic structure in the kernel allocate through it. It comes up in two phases: a small fixed buffer available before paging and the frame allocator exist, and then a larger frame-backed region mapped at a fixed virtual base. Both phases drive the same allocator, which zeroes memory on allocation and on free. The code is under src/memory/heap/.

The global allocator

The heap is the Rust global allocator (src/memory/heap/manager/globals.rs:21):

  #[global_allocator]
  static KERNEL_HEAP: SecureHeapAllocator = SecureHeapAllocator::new();

Because it is marked #[global_allocator], every alloc-crate allocation in the kernel routes to it, which is why bringing it up early in boot is what makes Vec and friends available to the rest of initialisation. The allocator type is SecureHeapAllocator (src/memory/heap/types/); the "secure" is the zeroing policy described below.

The two phases

The heap starts in bootstrap mode. A fixed static buffer is reserved in the kernel image (globals.rs:29):

  static mut BOOTSTRAP_HEAP_MEMORY: BootstrapHeapMemory =
      BootstrapHeapMemory { data: [0u8; BOOTSTRAP_HEAP_SIZE] };
  static USING_BOOTSTRAP: AtomicBool = AtomicBool::new(true);

USING_BOOTSTRAP begins true, so the earliest allocations, made before the frame allocator and paging are up, are served from this fixed buffer built into the image. It is small and fixed, so it is only meant to carry init far enough to map the real heap. Once that is done, init flips USING_BOOTSTRAP to false (init.rs:38) and every subsequent allocation is served from the mapped region.

Bringing up the real heap

init (src/memory/heap/manager/init.rs:26) allocates the backing frames, maps them at the heap's virtual base, and hands the region to the allocator:

  init():
      if the heap is already initialised -> Ok
      heap_size  = layout::KHEAP_SIZE
      heap_pages = ceil(heap_size / PAGE_SIZE)
      frames     = allocate_heap_frames(heap_pages)     one frame per page
      heap_start = map_heap_memory(frames)              map at KHEAP_BASE, R|W
      KERNEL_HEAP.init(heap_start, heap_size)
      HEAP_STATS.set_total_size(heap_size)
      USING_BOOTSTRAP = false

allocate_heap_frames (init.rs:42) pulls one frame at a time from the frame allocator and returns HeapError::FrameAllocationFailed if any request comes back empty, so a heap that cannot be fully backed fails init rather than coming up partially. map_heap_memory (init.rs:53) maps each frame in turn at KHEAP_BASE + i * PAGE_SIZE with READ | WRITE permissions through the paging manager, returning HeapError::MappingFailed on any failure. The heap is therefore a contiguous virtual range at a fixed base, backed by frames that need not be contiguous in physical memory. init is idempotent: a second call returns Ok immediately once the heap is initialised.

Zeroing on allocation and free

Two flags govern the allocator's zeroing policy, and both default to on (globals.rs:24):

  HEAP_ZERO_ON_ALLOC  AtomicBool = true
  HEAP_ZERO_ON_FREE   AtomicBool = true

With zero-on-alloc, memory handed out never carries the contents of a previous allocation, so a bug that reads uninitialised memory sees zeros rather than stale kernel data. With zero-on-free, memory is wiped as it is returned, so freed data does not sit in the heap waiting to be read back through a later allocation. This is the same defence-in-depth posture the ZeroState model applies to capsule memory, applied here to the kernel's own heap, and it is what the "secure" in SecureHeapAllocator names.

Statistics and time

The heap keeps HEAP_STATS (HeapStatistics) with the total size set at init and allocation counts maintained thereafter, and get_timestamp (globals.rs:32) reads the TSC directly for timestamping, since the heap can be exercised before the higher time bases are calibrated.

Errors

The heap error type (src/memory/heap/error/) surfaces two failures from init: FrameAllocationFailed when the frame allocator cannot back the whole heap, and MappingFailed when a page cannot be mapped at the heap base.

Where this connects

The heap depends on the frame allocator for its backing pages and the paging manager to map them, so it is brought up in init_unified_vm's wake during boot, after those two are live and before the rest of init needs dynamic allocation. Its virtual base and size are layout::KHEAP_BASE and layout::KHEAP_SIZE in the memory layout constants.

Security analysis

The heap is a shared kernel resource, so its safety properties are about not leaking one allocation's data into the next and not coming up in a half-mapped state. Two properties carry that, and one bound is worth naming.

Zero on both ends. HEAP_ZERO_ON_ALLOC and HEAP_ZERO_ON_FREE both default to true (globals.rs:24). Zero-on-alloc means memory handed out never carries a previous allocation's bytes, so a bug that reads uninitialised memory sees zeros rather than stale kernel data, secrets or pointers included. Zero-on-free means a freed block is wiped as it is returned, so data does not sit in the heap waiting to be read back through a later allocation. This is the "secure" in SecureHeapAllocator, and it is the zeroization posture applied to the kernel's own heap rather than to capsule frames. It is a single zero pass, not the multi-pass DoD erase, which is the right cost for steady-state reclaim.

All-or-nothing bring-up. init (init.rs:26) pulls one frame at a time through allocate_heap_frames and returns HeapError::FrameAllocationFailed the moment any request comes back empty (init.rs:47), and map_heap_memory returns HeapError::MappingFailed if any page fails to map (init.rs:58). So a heap that cannot be fully backed fails init loudly rather than coming up partially mapped, where a later allocation would fault on an unbacked page. init is idempotent, a second call returns Ok once initialised, so re-entry cannot double-map the base.

The honest boundary is that the heap frames are mapped READ | WRITE with no guard pages between allocations, so the heap relies on the allocator's own bounds and the hardening tracker rather than on hardware isolation between neighbouring heap objects. The isolation the heap does give you is against the previous tenant of a block (through zeroing), not against a live neighbour overrunning into you.

Debugging the heap

The heap surfaces exactly two failures from init, both HeapError variants (error/types.rs), and they point at different layers:

  FrameAllocationFailed   the frame allocator could not back the whole heap (physical exhaustion)
  MappingFailed           a page could not be mapped at KHEAP_BASE (paging manager rejected it)

A FrameAllocationFailed at heap init is a bottom-of-the-stack problem: the frame allocator ran out before it could hand over KHEAP_SIZE / PAGE_SIZE frames, so the fix is upstream (a bad memory-map seed, or the heap sized larger than the managed range). A MappingFailed means the frames existed but the paging manager refused a mapping at the heap base, which points at a layout or W^X problem rather than exhaustion. Because init is the only place these are returned, a heap fault after init is not a heap-init bug, it is an ordinary allocation reading or writing past its bounds, which is the hardening tracker's and the page-fault handler's territory. The heap's HEAP_STATS (total size set at init, allocation counts thereafter) is the runtime view: a live allocation count that never drops while memory pressure climbs is a leak in a caller, not in the heap.

Where this connects

The heap depends on the frame allocator for its backing pages and the paging manager to map them, so it is brought up in init_unified_vm's wake during boot, after those two are live and before the rest of init needs dynamic allocation. Its virtual base and size are layout::KHEAP_BASE and layout::KHEAP_SIZE in the memory layout constants. The zeroing policy is the per-heap arm of the zeroization posture, and overrun detection over heap ranges lives in the hardening tracker.

Source map

  src/memory/heap/manager/globals.rs  the global allocator, bootstrap buffer, flags
  src/memory/heap/manager/init.rs      init, allocate_heap_frames, map_heap_memory
  src/memory/heap/types/               SecureHeapAllocator and HeapStatistics
  src/memory/heap/error/types.rs       HeapError
  src/memory/layout/                   KHEAP_BASE and KHEAP_SIZE

Every reference above is verified against those trees. The frames come from the physical frame allocator, the mapping goes through the paging manager, the zeroing is the zeroization posture, and heap-range corruption detection is on the hardening page.