# 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**. - `TMemoryBlock` stores `Address` (physical) and `Used` (boolean). - `init()` walks the Multiboot memory map and marks blocks free/used. - `alloc_block` / `free_block` linearly 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, and `invlpg`s. - `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 `uint32` at 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 `Entries` array (`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 of `Free = true` entries, marks them `Used`, returns `base + 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` ```pascal 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` ```pascal 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` ```pascal 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 via `NextFreeHint` + dword-level BSF scan. Returns 0 as OOM sentinel. - `free_block`: O(1) direct bit clear with double-free detection. - `alloc_block`: Validates `PhysPresent` and `PhysAlloc` bits 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: 1. **`free_page` block extraction** — Was using raw Address field; fixed to `Address SHR 10`. 2. **`invlpg` operand** — Was passing PDE index; fixed to `page_number SHL 22` (virtual address). 3. **PDE clear** — Now zeroes the PDE entry before returning block to PMM. 4. **`map_page` double-write** — Removed redundant direct PDE writes; delegates to `map_page_ex`. 5. **OOM propagation** — `new_page` returns false if PMM returns 0; missing `pop_trace` added. 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 - `kalloc` now calls `BSOD('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 (`klalloc` returns nil) also triggers BSOD from kalloc. #### 4b. Double-free detection ✅ - `kfree` verifies bitmap bits are set before clearing (`bitmap_all_set`). - If already clear → syslog error + GPF. #### 4c. Guard bytes (`{$IFDEF DEBUG_LMM}`) ✅ - Compile with `-dDEBUG_LMM` to enable. - `kalloc` allocates one extra ALLOC_UNIT and writes `$DEADBEEF` at the end. - `kfree` verifies 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_block` in PMM already validates `PhysPresent` bit. ### Phase 5 — Observability ✅ #### 5a. PMM stats ✅ - `pmm_free_blocks`: popcount of free bits across `PhysPresent AND NOT PhysAlloc`. - `pmm_total_blocks`: popcount of `PhysPresent`. #### 5b. LMM stats ✅ - `lmm_total_free`: sum of `FreeUnits × ALLOC_UNIT` across all heap pages. - `lmm_page_count`: count of heap pages in the linked list. #### 5c. MEMINFO command ✅ - Moved from inline `kernel.pas` to `src/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 |