Phase 53
Kernel Memory Modernization
The kernel memory subsystem becomes SMP-scalable and auditable: per-CPU page caching removes the global frame-allocator lock from the common single-page path, a magazine-based slab allocator with out-of-line slab metadata replaces the linked-list heap for small kernel objects, and the allocator preserves Phase 52b's zero-before-user-exposure guarantee while eliminating the double-lock and O(n) buddy removal paths.
Learning goal
Understand why per-CPU caching is the dominant design pattern in every production kernel allocator
Category
infrastructure
Components touched
Builds on
Unlocked by this phase
Why this phase matters
The kernel memory subsystem becomes SMP-scalable and auditable: per-CPU page caching removes the global frame-allocator lock from the common single-page path, a magazine-based slab allocator with out-of-line slab metadata replaces the linked-list heap for small kernel objects, and the allocator preserves Phase 52b’s zero-before-user-exposure guarantee while eliminating the double-lock and O(n) buddy removal paths.
How it works in m3OS
Phase 53a replaces the old “everything goes through one lock” kernel allocator stack with an SMP-aware design. Where Phase 33 introduced a correct buddy allocator, heap growth, and slab scaffolding, Phase 53a turns those pieces into a coherent hot-path allocator: per-CPU page caches take most single-page traffic off the global buddy lock, magazines cache small objects per CPU, a size-class GlobalAlloc routes ordinary kernel allocations away from the linked-list heap, cross-CPU frees stop taking the victim CPU’s slab lock, and allocator-local reclaim flushes hidden pages and objects before high-order allocation or OOM failure.
How it builds on earlier phases
This phase builds on Headless Hardening and turns that foundation into Kernel Memory Modernization.
Implementation outline
- Track D (foundation fixes): Fix double-lock, O(n) remove_free, refcount ordering, and restate the exact zero-before-exposure guarantee before changing zeroing behavior.
- Track F.1 (contract): Define IRQ/atomic-context rules, reclaim hooks, and observability surfaces before replacing hot paths.
- Track A (per-CPU page cache): Add per-CPU frame lists, modify
allocate_frame()/free_frame()to check per-CPU first, and implement coordinated owner-CPU drain/reclaim. - Track B (slab + magazines): Rewrite
kernel-core/src/slab.rswith embedded freelist, out-of-line metadata, and the magazine layer. Create size-class caches. - Track C (GlobalAlloc replacement): Wire size-class slab caches into a new
GlobalAllocimplementation, define bootstrap consumers/cutover, and migrate stats / meminfo surfaces. - Track E (cross-CPU free): Add atomic free list per CPU per size class and implement MPSC push + batch collection.
- Track F.2-F.4 (integration): Add allocator-local reclaim, kill switch, and final stress / compatibility validation.
Task progress snapshot
107 of 107 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- NUMA-aware per-domain slab and page caches (forward-compatible design, implement when NUMA target exists)
- Constructor/destructor object caching (add when profiling shows object init is a bottleneck)
- Full memory debugging suite (red zones, poison fill, KFENCE-style sampling) — planned for a later sub-phase or Phase 53
- Memory pressure callbacks (shrinker interface) for non-allocator caches — deferred to Phase 54; 53a only adds allocator-local drain/reclaim hooks
- Full GFP-like allocation-context flags — 53a defines a minimal sleepable vs IRQ-sensitive contract, richer policy comes later
- Type-state
Frames<Free/Allocated/Mapped>wrappers — high value but large refactor surface; evaluate after core allocator lands
Source materials
Important code
Code spotlight cards
Code spotlight
Cache phys_offset in a static to eliminate double-lock in free_frame
Cache phys_offset in a static to eliminate double-lock in free_frame. Why it matters: free_frame() currently acquires the frame allocator lock twice — once to read phys_offset (a constant after init) and once to call free_to_pool(). The first acquisition is unnecessary and doubles lock contention on every free. Focus on `free_frame` and `phys_offset` in `kernel/src/mm/frame_allocator.rs` while you trace the snippet.
/// Return a frame to the allocator.
///
/// If refcounting is initialized, decrements the reference count first.
/// The frame is only reclaimed when the count reaches zero. Frames allocated
/// before refcounting was enabled (refcount == 0) are freed directly.
///
/// # Minimal allocation-context contract (F.1)
///
/// - [`AllocationContext::IrqSensitive`] callers mutate the local page cache
/// with interrupts masked and a same-core guard. A re-entrant free bypasses
/// the cache and returns directly to the buddy/free-list pool.
/// - [`AllocationContext::Sleepable`] callers may tolerate cache-overflow
/// drains that briefly spin on the global allocator lock.
///
/// # Slow-path behavior
///
/// When the cache exceeds the high watermark, the allocator drains a batch of
/// 32 frames back to the buddy. This path does **not** sleep and does not
/// retry; it may briefly spin on `FRAME_ALLOCATOR`.
///
/// Does **not** zero the frame — the zero-before-exposure invariant is
/// enforced on the allocation side via [`allocate_frame_zeroed`]. Panics on
/// double-free (frame already on the free list).
pub fn free_frame(phys: u64) { Code spotlight
Replace O(n) buddy removal with an O(1)-removable free-block representation
Replace O(n) buddy removal with an O(1)-removable free-block representation. Why it matters: remove_free does an O(n) linear scan of a Vec<usize> to find and remove a buddy during coalescing. With thousands of free blocks, this defeats the O(log n) guarantee. The replacement must preserve kernel-core host testability instead of assuming direct access to real free-page bodies inside the pure data-structure crate. Focus on `BuddyAllocator::free_lists` and `BuddyAllocator::remove_free` in `kernel-core/src/buddy.rs` while you trace the snippet.
/// Remove a specific PFN from the free pool at `order` in O(1) by clearing
/// its leaf bit plus the affected summary words.
fn remove_free(&mut self, order: usize, pfn: usize) {
if !self.is_free(order, pfn) {
debug_assert!(
false,
"BuddyAllocator::remove_free: pfn {} not found in free map for order {}",
pfn, order,
);
return;
}
self.clear_free(order, pfn);
self.free_counts[order] -= 1;
} Code spotlight
Add freelist pointer hardening
Add freelist pointer hardening. Why it matters: Embedded freelist pointers inside free objects are a target for heap corruption attacks. XOR-encoding with a per-cache random value and the object address makes exploitation significantly harder with near-zero overhead (two XOR instructions). Focus on `encode_freeptr` and `decode_freeptr` in `kernel-core/src/slab.rs` while you trace the snippet.
Key files
- kernel/src/mm/frame_allocator.rs — Per-CPU page cache, refcount ordering, zero-before-exposure helpers, high-order reclaim retry
- kernel/src/mm/slab.rs — Kernel magazine fast paths, cross-CPU routing, reclaim ordering, empty-slab return
- kernel/src/mm/magazine.rs — A fixed-size array of physical frame addresses per CPU, accessed via smp::per_core(). Cooperative scheduling removes kernel task migration during the fast path, but IRQ re-entrancy still exists, so local mutations occur with interrupts masked or an equivalent local non-reentrancy guard. The per-CPU cache sits between callers and the global BuddyAllocator, absorbing the vast majority of single-page allocations while coordinated reclaim drains caches via owner-CPU self-drain or equivalent synchronized handoff.
- kernel-core/src/slab.rs — Embedded-freelist slab cache with D.5 pointer hardening and out-of-line span metadata
- kernel/src/mm/heap.rs — Size-class GlobalAlloc, dense PageMeta, bootstrap fallback, allocator-local reclaim coordination
- kernel-core/src/buddy.rs — Host-testable buddy allocator with summary bitmaps for fast free-block tracking
- kernel/src/mm/* — :position()-based free-block removal with an O(1)-removable per-order representation while preserving kernel-core as a pure, host-testable data structure. If page-body metadata is used, keep it in the kernel-side wrapper rather than baking physical-page assumptions into kernel-core.
- kernel/src/fs/procfs.rs — /proc/meminfo reporting for the new allocator layers
- kernel/src/arch/x86_64/syscall/mod.rs — :position()-based free-block removal with an O(1)-removable per-order representation while preserving kernel-core as a pure, host-testable data structure. If page-body metadata is used, keep it in the kernel-side wrapper rather than baking physical-page assumptions into kernel-core.
- kernel-core/src/magazine.rs — Fixed-capacity magazines and shared depot for per-CPU object caching
- kernel-core/src/size_class.rs — Exact 13-class table and waste-bound tests
- kernel-core/src/cross_cpu_free.rs — Lock-free intrusive MPSC queue for cross-CPU slab frees
- userspace/coreutils-rs/src/meminfo.rs — User-visible memory reporting consistent with the new accounting policy
- kernel/src/main.rs — QEMU regression coverage for allocator-local reclaim and related phase tests
How real OSes differ
- Linux SLUB uses a lockless CAS+tid scheme for per-CPU allocation; m3OS uses the simpler CPU-local fast path with interrupt masking because it lacks kernel preemption but still must handle IRQ re-entrancy
- FreeBSD UMA supports constructor/destructor caching for objects with expensive initialization; Phase 53a defers this as it adds complexity with uncertain benefit for m3OS's current object types
- Linux has 6 migration types for anti-fragmentation; m3OS defers migration-type support as it's primarily valuable for memory compaction and hotplug
- Production allocators support NUMA-aware per-domain caching; m3OS defers NUMA but the per-CPU design is forward-compatible
- Linux KASAN provides compile-time-instrumented shadow memory for all memory accesses; m3OS defers full sanitizer support but adds freelist pointer hardening
Success criteria
- `allocate_frame()` cache-hit fast path does not acquire any global lock and mutates only CPU-local state with interrupts masked or an equivalent local non-reentrancy guard
- Global cache drain / reclaim never performs unsynchronized remote mutation of another CPU's lockless cache
- Kernel object allocation (`Box::new`, `Vec::push`) routes through size-class slab caches for small objects, while layouts >4 KiB or requiring stronger alignment use a documented page-backed path
- Per-CPU magazine allocation completes without lock acquisition in the common case, with out-of-line slab metadata that keeps the 4 KiB size class usable
- `free_frame()` acquires the frame allocator lock at most once (not twice)
- Buddy free-block removal no longer linearly scans `Vec::position()`
- Refcount atomics use Acquire/Release ordering (not SeqCst)
- All existing QEMU tests pass unchanged (API-compatible)
- `cargo test -p kernel-core --target x86_64-unknown-linux-gnu` passes with new allocator unit tests, and loom coverage exists for new cross-CPU free / ordering-sensitive queues
- `frame_stats()`, `heap_stats()`, `/proc/meminfo`, and `sys_meminfo` show updated statistics reflecting the new allocator layers
- Cross-CPU free of slab objects does not acquire the victim CPU's slab lock
- Any frame that can become user-visible is zeroed before exposure, including contiguous and lazy-fault / CoW-sensitive paths that depend on allocator zeroing
- A bring-up kill switch or equivalent fallback exists while the new allocator is being integrated
Related phases
Keep exploring the roadmap
Phase 53
CompleteHeadless Hardening
m3OS becomes a deliberately operable headless/reference system: the security floor is repaired, the service model is trustworthy, the basic developer workflow is boringly repeatable, and the project has explicit validation gates for what it now claims to support.
Phase 54
CompleteDeep Serverization
m3OS moves beyond proof-of-concept extraction and pushes real storage, namespace, and networking responsibilities out of the kernel. The kernel becomes a thinner object and transport layer, while userspace services own more of the policy-heavy behavior that currently dominates the trusted computing base.