Memory Management Analysis¶
Living document for the PMM / VMM / LMM refactor. Last updated: 2025-07-11 — All phases complete.
Architecture Overview¶
Asuro's memory management is split into three cooperating units:
| Unit | File | Role |
|---|---|---|
| PMM | src/pmemorymanager.pas |
Physical page allocator — bitmap tracks which 4 MB physical blocks are free/used |
| VMM | src/vmemorymanager.pas |
Virtual memory mapper — manages x86 PSE 4 MB page directory entries |
| LMM | src/lmemorymanager.pas |
Logical (heap) allocator — bitmap-based kalloc/kfree for kernel objects |
Call flow: kalloc → LMM bitmap scan → if no space, new_heap_page → VMM new_page → PMM new_block.
1 Physical Memory Manager (PMM)¶
Current Design¶
- 1024-element static array (
Memory_Map: array[0..1023] of TMemoryBlock), each entry covers a 4 MB block → 4 GB addressable. TMemoryBlockstoresAddress(physical) andUsed(boolean).init()walks the Multiboot memory map and marks blocks free/used.alloc_block/free_blocklinearly scan the array.
Issues¶
| # | Severity | Description |
|---|---|---|
| 1 | Important | alloc_block scans linearly from index 0 every time — O(n) per allocation. Should maintain a hint/cursor or use a bitmap. |
| 2 | Important | free_block looks up by address using a linear scan — O(n). A direct index calculation (addr SHR 22) would be O(1). |
| 3 | Important | No OOM handling — alloc_block loops forever (while true) if all blocks are used. Should return a sentinel or nil. |
| 4 | Nice-to-have | Static array of 1024 × TMemoryBlock wastes space; a simple 1024-bit bitmap (128 bytes) would suffice. |
| 5 | Nice-to-have | The Address field stores (i * $400000) SHL 10 during init — the extra SHL 10 is suspicious and may cause mismatches with free_block's comparison. Needs audit. |
| 6 | Nice-to-have | No support for >4 GB (PAE). Fine for now but worth noting. |
2 Virtual Memory Manager (VMM)¶
Current Design¶
- x86 PSE (4 MB pages) — single-level page directory, no page tables.
alloc_page(vaddr)asks PMM for a physical block, writes the PDE, andinvlpgs.free_page(vaddr)clears the PDE and returns the physical block to PMM.map_page(paddr, vaddr)creates a fixed mapping without PMM involvement.- Page directory is a 1024-entry array of
uint32at a static address.
Issues¶
| # | Severity | Description |
|---|---|---|
| 1 | Critical | free_page extracts the physical block number incorrectly. It reads Address from the PMM block (which was stored with SHL 10 during PMM init), then applies SHR 10 — this should be SHR 22 to convert a physical address to a 4 MB block index. The current code frees the wrong block. |
| 2 | Critical | invlpg operand is wrong in free_page — it passes the page-directory index (block) instead of the virtual address. invlpg requires a virtual address. |
| 3 | Critical | Page directory may not be page-aligned. It's declared as a typed pointer (PuInt32) allocated via alloc_page, but PSE page directories must be 4 KB-aligned (they are, since alloc_page returns 4 MB-aligned blocks, but this is not enforced or documented). |
| 4 | Important | alloc_page does not check the PMM return value for failure — if PMM is exhausted (and we fix PMM to return a sentinel), VMM will write a garbage PDE. |
| 5 | Important | No TLB shootdown for multi-core. Single-core only for now, but worth noting. |
| 6 | Important | map_page doesn't check whether the PDE slot is already occupied — silently overwrites existing mappings. |
| 7 | Important | alloc_page sets PDE flags to $83 (Present + R/W + PSE) for all pages. No distinction between kernel/user pages. |
| 8 | Nice-to-have | No guard pages, no unmapped gaps between allocations. |
| 9 | Nice-to-have | free_page doesn't zero the page contents before returning to PMM — potential info leak if pages are reused by different subsystems. |
3 Logical Memory Manager (LMM)¶
Current Design¶
- Each "heap page" is a 4 MB region obtained from VMM (
klalloc). - Within each page, an
Entriesarray (MAX_ENTRIES = $60000= 393,216 entries) tracks allocation status. - Each entry corresponds to an 8-byte "allocation unit" (
ALLOC_SPACE = 8). kalloc(size)scans entries for a contiguous run ofFree = trueentries, marks themUsed, returnsbase + index * 8 + DATA_OFFSET.kfree(ptr)finds the matching range and marks entries free again.DATA_OFFSET = $100000— first 1 MB of each 4 MB page is reserved for the entry metadata.
Issues¶
| # | Severity | Description |
|---|---|---|
| 1 | Critical | klalloc uses decimal 4000000 instead of hex $400000 for the page size. 4000000 decimal = $3D0900 ≈ 3.8 MB, which is not a 4 MB-aligned allocation and will cause the VMM mapping to be misaligned. Should be $400000. |
| 2 | Critical | klfree is unimplemented (body is empty) — any klalloc'd page is leaked forever. The heap can only grow, never shrink. |
| 3 | Critical | No allocation type tag — kfree cannot distinguish between small allocations (served by kalloc from the entry array) and large allocations (served by klalloc directly). Calling kfree on a klalloc'd pointer will corrupt the entry array. |
| 4 | Important | kalloc linear scan is O(n) over MAX_ENTRIES (393K entries) per allocation — extremely slow for a hot path. Should use a bitmap or free-list. |
| 5 | Important | kalloc zeroes returned memory with a byte-by-byte loop. Should use FillDWord / rep stosd for 4× throughput. |
| 6 | Important | No mutex/lock — concurrent kalloc/kfree from interrupts or (future) threads will corrupt the entry array. |
| 7 | Important | MAX_ENTRIES × sizeof(THeapEntry) metadata overhead: each THeapEntry is ~6 bytes × 393K = ~2.3 MB of the 4 MB page used for bookkeeping — only ~1.7 MB usable per page (43% efficiency). |
| 8 | Important | Magic number validation in kfree (if Entries^[idx].Magic <> $600D → "Bad KFree") halts the kernel via psod. No graceful recovery. |
| 9 | Important | Fragmentation: after many alloc/free cycles, the linear scan will skip over many small free gaps, causing both slowdown and wasted space. No compaction or coalescing strategy. |
| 10 | Nice-to-have | DATA_OFFSET = $100000 (1 MB) is hardcoded but the actual metadata size depends on MAX_ENTRIES × sizeof(THeapEntry). If the record size changes, the offset may overlap with data or waste space. |
| 11 | Nice-to-have | The linked list of heap pages (THeapPage) is traversed linearly to find which page owns a pointer during kfree — O(pages) per free. |
| 12 | Nice-to-have | kalloc always starts scanning from entry 0 — a roving pointer (next-fit) would reduce average scan time. |
| 13 | Nice-to-have | No statistics tracking (total allocated, peak usage, fragmentation metrics). |
4 Cross-Unit Issues¶
| # | Severity | Description |
|---|---|---|
| 1 | Critical | PMM Address field stores (i * $400000) SHL 10 — this value does not match the raw physical address. VMM and LMM both assume they're getting a real physical address from PMM. If the stored value is actually phys SHL 10, every consumer is mapping the wrong physical memory. Needs immediate audit. |
| 2 | Important | No unified OOM path — PMM infinite-loops, VMM doesn't check, LMM doesn't check. A single allocation failure cascades into a hang. Should propagate nil up the chain. |
| 3 | Important | Memory is never returned to the system — klfree is a no-op, so VMM pages are never freed, so PMM blocks are never freed. The system can only allocate, never deallocate at the page level. |
| 4 | Nice-to-have | No memory map dump / debug command to inspect PMM/VMM/LMM state at runtime. |
5 Public Interface Contracts (preserved)¶
All callers outside the three memory units use these entry points. The refactor preserved identical names & signatures so no consumer code needed to change.
PMM — pmemorymanager¶
procedure init;
function alloc_block(block : uint16; caller : uint32) : boolean;
procedure force_alloc_block(block : uint16; caller : uint32);
function new_block(caller : uint32) : uint16; { returns 0 on OOM }
procedure free_block(block : uint16; caller : uint32);
function pmm_free_blocks : uint32; { popcount of free bits }
function pmm_total_blocks : uint32; { popcount of PhysPresent }
VMM — vmemorymanager¶
procedure init;
function new_page(page_number : uint16) : boolean;
function page_mappable(page_number : uint16) : boolean;
function map_page(page_number : uint16; block : uint16) : boolean;
function map_page_ex(page_number : uint16; block : uint16; PD : PPageDirectory) : boolean;
function new_page_at_address(address : uint32) : boolean;
procedure free_page(page_number : uint16);
procedure free_page_at_address(address : uint32);
function new_page_directory : uint32;
function new_kernel_mapped_page_directory : uint32;
function vtop(address : uint32) : uint32;
LMM — lmemorymanager¶
procedure init;
function kalloc(size : uint32) : void; { alias 'kernel_kalloc' }
function klalloc(size : uint32) : void;
procedure klfree(address : uint32);
function kpalloc(address : uint32) : void;
procedure kfree(area : void); { alias 'kernel_kfree' }
function lmm_total_free : uint32; { sum of FreeUnits×8 across pages }
function lmm_page_count : uint32; { number of heap pages }
6 Refactor Completion Summary¶
Phase 1 — PMM Rewrite ✅¶
Replaced ~8 KB record array with compact bitmaps (384 bytes total):
| Structure | Size | Purpose |
|---|---|---|
PhysPresent[0..31] |
128 B | Which blocks exist (from multiboot) |
PhysAlloc[0..31] |
128 B | Which blocks are allocated |
PhysScanned[0..31] |
128 B | Walk-once optimisation |
PhysOwner[0..1023] |
4 KB | Per-block owner tracking |
new_block: O(1) amortised viaNextFreeHint+ dword-level BSF scan. Returns 0 as OOM sentinel.free_block: O(1) direct bit clear with double-free detection.alloc_block: ValidatesPhysPresentandPhysAllocbits before allocation.- Stats:
pmm_free_blocks(popcount),pmm_total_blocks. - First 4 blocks (0–3, 16 MB) reserved for kernel/BIOS.
Phase 2 — VMM Correctness Fixes ✅¶
Five bugs fixed:
free_pageblock extraction — Was using raw Address field; fixed toAddress SHR 10.invlpgoperand — Was passing PDE index; fixed topage_number SHL 22(virtual address).- PDE clear — Now zeroes the PDE entry before returning block to PMM.
map_pagedouble-write — Removed redundant direct PDE writes; delegates tomap_page_ex.- OOM propagation —
new_pagereturns false if PMM returns 0; missingpop_traceadded.
Critical discovery: free_page was always broken before our fix — virtual pages could never
be freed, making try_release_page in the LMM a no-op.
Phase 3 — LMM Rewrite ✅¶
Complete rewrite with bitmap allocator. Per-page layout:
[0x000000..0x000017] THeapPageHeader (24 bytes)
[0x000018..0x00FC17] Bitmap (64512 bytes = 516096 bits)
[0x00FC18..0x00FFFF] Padding (1000 bytes)
[0x010000..0x3FFFFF] Data area (4128768 bytes = 516096 × 8)
~93% page efficiency (up from ~43%).
Key constants:
- ALLOC_UNIT = 8 — bytes per allocation unit
- SIZE_PREFIX = 16 — 4B size + 12B padding for 16-byte alignment (SSE MOVAPS requirement)
- DATA_OFFSET = $10000 — data area starts at 64 KB into each page
- LARGE_ALLOC_MAGIC = $FFFFFFFE — sentinel for klalloc'd multi-page allocations
Interrupt safety: pushfd; cli; pop saved_flags (stack-balanced) + restore_if(saved_flags)
inline procedure that only restores IF via sti, avoiding dangerous EFLAGS restoration
(IOPL, NT, VM, AC bits that caused Bad-TSS on IRET).
try_release_page: Returns completely-free heap pages to VMM/PMM (unlinks from doubly-linked
list, fixes Search_Page, calls free_page_at_address). Skips Root_Page.
Phase 4 — Integration & Hardening ✅¶
4a. OOM path — BSOD¶
kallocnow callsBSOD('OOM', ...)instead of returning nil on OOM.- 65 of 72 callers (90%) don't check nil — a nil return would cascade into undefined behaviour. Kernel panic is the correct response.
- Large allocation path (
klallocreturns nil) also triggers BSOD from kalloc.
4b. Double-free detection ✅¶
kfreeverifies bitmap bits are set before clearing (bitmap_all_set).- If already clear → syslog error + GPF.
4c. Guard bytes ({$IFDEF DEBUG_LMM}) ✅¶
- Compile with
-dDEBUG_LMMto enable. kallocallocates one extra ALLOC_UNIT and writes$DEADBEEFat the end.kfreeverifies the guard sentinel; corrupted guard → syslog error + GPF.- Detects buffer overrun corruption.
4d. kpalloc validation ✅¶
- Bounds check: block index must be < 1024 (GPF otherwise).
- Logs identity-map operations via syslog for MMIO auditing.
alloc_blockin PMM already validatesPhysPresentbit.
Phase 5 — Observability ✅¶
5a. PMM stats ✅¶
pmm_free_blocks: popcount of free bits acrossPhysPresent AND NOT PhysAlloc.pmm_total_blocks: popcount ofPhysPresent.
5b. LMM stats ✅¶
lmm_total_free: sum ofFreeUnits × ALLOC_UNITacross all heap pages.lmm_page_count: count of heap pages in the linked list.
5c. MEMINFO command ✅¶
- Moved from inline
kernel.pastosrc/prog/meminfo.pas(following prog pattern). - Registered via
progmanager.pas→meminfo.init(). - Displays: Multiboot memory, PMM block stats, LMM heap stats (pages, size, free).
7 Bugs Fixed During Refactor¶
| Bug | Root Cause | Fix |
|---|---|---|
| Bad TSS exception during network processing | pushfd/popfd across separate asm blocks: FPC doesn't track ESP changes in asm, causing EFLAGS corruption (NT=1, IOPL wrong) → Bad TSS on IRET |
Stack-balanced pushfd; cli; pop local_var + restore_if() that only does sti if IF was set |
| GPF from SSE MOVAPS in doublebuffer flush | SIZE_PREFIX=4→8 gave 8-byte alignment; MOVAPS needs 16-byte. klalloc returned page+8 (not 16-aligned) |
SIZE_PREFIX=16 (4B size + 12B pad), klalloc header expanded to 16 bytes |
| Pages never freed (historic) | free_page extracted wrong block index from PDE Address field |
Fixed extraction to Address SHR 10 — page freeing works for the first time |
8 Risk Assessment (Post-Refactor)¶
| Area | Status | Notes |
|---|---|---|
| PMM bitmap | ✅ Validated | Full boot, 664 unit tests pass |
| VMM page free | ✅ Validated | try_release_page exercises free path |
| LMM bitmap allocator | ✅ Validated | All kernel subsystems functional |
| Large allocations (klalloc) | ✅ Validated | E1000, USB, LVGL all use large allocs |
| Interrupt safety | ✅ Validated | Network IRQs during allocation work correctly |
| SSE alignment | ✅ Validated | Doublebuffer flush (MOVAPS) works |