diff --git a/docs/re/mission-freeze-heap-exhaustion.md b/docs/re/mission-freeze-heap-exhaustion.md new file mode 100644 index 00000000..0c87bfce --- /dev/null +++ b/docs/re/mission-freeze-heap-exhaustion.md @@ -0,0 +1,78 @@ +# The mission freeze is a failed 128 MB guest allocation + +**โœ… Root-caused 2026-08-26** on a Stage 02 run. The freeze this corpus lists as +"THE blocker" is not a hang in the emulator: it is the *guest* asking for 128 MB +of physical memory, being refused, throwing, and stopping. + +## The freeze, measured rather than assumed + +Two independent witnesses, both taken while `screen_id.py` was calling the +screen `flight`: + +| | | +|---|---| +| three screenshots over 8 s | **rmse 0.00**, `0.00 %` of pixels changed | +| a 4 MB slab of guest RAM re-read after 2 s | **0 bytes changed** | + +The emulator process is *not* deadlocked โ€” 399 % CPU across 15 running threads. +It is spinning while the guest does nothing. + +## The cause is in the emulator log, and it is specific + +The log stops dead mid-stage-load, last four lines: + + BaseHeap::Alloc failed to find contiguous range + PhysicalHeap::AllocRange unable to alloc physical memory in parent heap + (requested 134217728 bytes, parent free 28969/131072 pages) + MmAllocatePhysicalMemoryEx: Allocation failed: 00000000 Size: 08000000 + Guest attempted to throw a C++ exception! + +`0x08000000` is **128 MB**. The parent heap holds 131 072 pages and had **28 969 +free โ€” about 113 MB**. So this is not only fragmentation, as +`failed to find contiguous range` suggests on its own: there was **less free +memory in that heap than the request needed**, contiguous or not. + +Immediately before it, repeatedly: + + BaseHeap::Release failed because address is not a region start + PhysicalHeap::Release failed due to parent heap failure + +[`challenge-mission-gate.md`](challenge-mission-gate.md) already noted that this +failed release "leaks the range". A leak that repeats through a session gives the +mechanism the freeze's **variable onset** (27, 45, 83, 183 s across runs) needs: +free pages drain until some 128 MB request loses, and *when* that happens depends +on how much has leaked, not on the clock. + +## ๐Ÿ”ด This refutes "poking only real story ids does not blow the heap" + +`challenge-mission-gate.md` ยง5.6 records that poking `0xFFFFFFFF` into the +cleared-stage mask blows the heap on a 128 MB request, and concludes: + +> Poking only real story ids (`0x0001FFFE` = stages 1โ€“16) does **not** blow the heap. + +This run poked **`0x0001FFFE`** โ€” the value called safe โ€” and hit the **same 128 MB +failure**, with `parent free 28969/131072` against that page's `30633/131072`. + +๐ŸŸก The two are not identical and the refutation is bounded: there, the failure was +on entering `MISSION SELECT`; here `MISSION SELECT` and the stage list worked, and +the failure came later, during the **take-off load**. So the correct reading is +not "the safe value is unsafe" but that **the poke value is not what decides it** โ€” +both values fail the same 128 MB request, at whichever load happens to arrive when +free pages have drained below it. That page's own open question โ€” *"repeat without +the poke to see whether it fails the same way regardless"* โ€” is now the load-bearing +experiment, and it is still unrun. + +## โš ๏ธ A frozen game passes the screen classifier + +`AGENT.md` says to classify screens by whole-image statistics rather than named +pixels, and that is right. But a *single-frame* statistic cannot tell **flight** +from **frozen in flight** โ€” `screen_id.py` called every frozen frame `flight`, +which is exactly why the entity probes returned 0 definitions, 0 movers and 0 +vtable hits with no indication anything was wrong. + +**A second frame costs nothing and is decisive.** Any probe that assumes the game +is live should diff two frames a few seconds apart, or re-read a slab of guest +RAM, before believing an empty result means "the thing is not there". + +โ” Still open: why the guest needs a single 128 MB block, whether the heap can be +given more, and whether the failure is reachable without the mask poke at all.