From 419d45eae55dcc4ebf8996c5db94ea07b702da7d Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 11:32:34 +0000 Subject: [PATCH] re: the leaking release is confirmed in Xenia's source, not inferred The previous write-up argued the leak from adjacency -- 22 failed releases next to the failed allocation. memory.cc closes the loop outright: * the "parent free N/M pages" in the error is parent_heap_->unreserved_page_count() (memory.cc:1807); * unreserved_page_count_ is incremented in exactly ONE place, memory.cc:1445, inside BaseHeap::Release's page-table loop; * the failing path returns at memory.cc:1399, at the top of that same function, before the loop -- page table untouched, no free block inserted; * and PhysicalHeap::Release delegates to parent_heap_->Release, so the release that fails and the allocation that later comes up short are the same heap. So every "address is not a region start" returns zero pages to the counter the allocator consults, and those pages stay reserved for the life of the process. That is control flow, not correlation. Magnitude is still open and I am not claiming it: 512 - 113 = 399 MB missing against only 22-23 failed releases would need ~18 MB average each, which is implausible as the whole story. Leaked releases are a contributor, maybe not the dominant one. An allocation ledger -- log every MmAllocatePhysicalMemoryEx and MmFreePhysicalMemory with sizes and balance them -- would settle it, and is a better use of a run than reproducing the freeze again. Also recorded so nobody hunts for it: there is NO cvar for guest memory size. memory.cc has only protect_zero / protect_on_release / scribble_heap and the MMIO ones, and xboxkrnl_memory.cc says "We don't support separate devkit memory, so just ignore this flag". 512 MB is hardcoded to the retail console, so the freeze cannot be dodged by giving the emulator more -- a fix has to be the release path itself. --- docs/re/mission-freeze-heap-exhaustion.md | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/re/mission-freeze-heap-exhaustion.md b/docs/re/mission-freeze-heap-exhaustion.md index 87240dbc..a7a6282c 100644 --- a/docs/re/mission-freeze-heap-exhaustion.md +++ b/docs/re/mission-freeze-heap-exhaustion.md @@ -122,6 +122,77 @@ So `parent heap` is **the emulated console's physical memory**, not anything of the host's. The game runs a real 512 MB console down to 113 MB free and then asks for 128 MB. Nothing about the container is involved. +## ✅ The mechanism, confirmed in Xenia's source — the failed release leaks, provably + +The adjacency argument below is no longer the evidence. `src/xenia/memory.cc` +closes the loop by itself. + +**1. The number in the error is `unreserved_page_count_`.** The message is +emitted at `memory.cc:1807`: + +```cpp +XELOGE("PhysicalHeap::Alloc unable to alloc physical memory in parent heap " + "(requested {} bytes, parent free {}/{} pages)", + size, parent_heap_->unreserved_page_count(), + parent_heap_->total_page_count()); +``` + +So the `28969/131072` in the log *is* that counter, on the **parent** heap. + +**2. That counter is incremented in exactly one place** — `memory.cc:1445`, +inside `BaseHeap::Release`'s page-table loop: + +```cpp +for (uint32_t page_number = base_page_number; page_number <= end_page_number; + ++page_number) { + auto& page_entry = page_table_[page_number]; + page_entry.qword = 0; + unreserved_page_count_++; // <- the only increment +} +InsertFreeBlock(base_page_number, base_page_entry.region_page_count); +``` + +**3. The failing path returns before reaching it** — `memory.cc:1399`, the very +top of the same function: + +```cpp +if (base_page_entry.base_address != base_page_number) { + XELOGE("BaseHeap::Release failed because address is not a region start"); + return false; // <- page table untouched, no free block +} +``` + +**4. And it is the same heap.** `PhysicalHeap::Release` delegates to +`parent_heap_->Release(...)`, so the release that fails and the allocation that +later comes up short are against the same 512 MB parent heap. + +Therefore every `address is not a region start` is a release that returns +**zero** pages to the counter the allocator later consults. The pages stay +reserved for the life of the process. This is not an inference from adjacency — +it is the control flow. + +## 🟡 What is still not measured: the magnitude + +Mechanism confirmed, quantity not. 512 − 113 = **399 MB** is unaccounted for, and +there are only **22–23** failed releases per run, which would need an implausible +~18 MB average each to explain the whole deficit alone. So leaked releases are +**a** contributor and possibly not the dominant one; the game may simply hold a +lot of live memory at stage load. + +The experiment that would settle it is an allocation ledger — log every +`MmAllocatePhysicalMemoryEx` / `MmFreePhysicalMemory` with its size and balance +the books — rather than more freeze reproductions. + +## 🔴 There is no memory-size knob to work around it + +Worth stating so nobody looks: Xenia has **no cvar for guest memory size**. The +only memory cvars in `memory.cc` are `protect_zero`, `protect_on_release`, +`scribble_heap` and the MMIO ones, and +`xboxkrnl_memory.cc` says outright *"We don't support separate devkit memory, so +just ignore this flag"* when a game asks for the devkit's extra RAM. The 512 MB +is hardcoded to the retail console. So the freeze cannot be dodged by giving the +emulator more — the fix has to be the release path, in a canary build. + ## 🟡 The likely mechanism: a release path that leaks Immediately before the failed allocation, in **both** runs: