From 78f097ef1f5d3f40b4c245e9339ba31e067b6230 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 11:14:24 +0000 Subject: [PATCH] re: the mission freeze is a failed 128 MB guest allocation, not an emulator hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduced on a Stage 02 run and root-caused. Two witnesses, both taken while screen_id.py was calling the screen "flight": three screenshots over 8 s at rmse 0.00 with 0.00% of pixels changed, and a 4 MB slab of guest RAM with 0 bytes changed over 2 s. The emulator is not deadlocked -- 399% CPU over 15 running threads. It spins while the guest does nothing. The log stops mid-stage-load on: PhysicalHeap::AllocRange unable to alloc physical memory in parent heap (requested 134217728 bytes, parent free 28969/131072 pages) MmAllocatePhysicalMemoryEx: Allocation failed Size: 08000000 Guest attempted to throw a C++ exception! 128 MB requested against ~113 MB free. So it is not only fragmentation, which is what "failed to find contiguous range" suggests on its own -- there was less free memory in that heap than the request needed at all. Preceded by repeated "BaseHeap::Release failed because address is not a region start", which challenge-mission-gate.md already notes leaks the range; a leak that repeats through a session supplies the mechanism the freeze's variable onset (27, 45, 83, 183 s) needs. This refutes a standing claim. challenge-mission-gate.md ยง5.6 concludes that poking only real story ids (0x0001FFFE) does not blow the heap. This run poked exactly that and hit the same 128 MB failure. Bounded, though: there the failure was on entering MISSION SELECT, here MISSION SELECT worked and the failure came at the take-off load -- so the reading is that the poke value is not what decides it. That page's own open question, repeat without the poke, is now the load-bearing experiment and is still unrun. Also recorded: a frozen game passes the screen classifier. A single-frame statistic cannot distinguish flight from frozen-in-flight, which is why the entity probes returned 0 definitions, 0 movers and 0 vtable hits with no sign anything was wrong. A second frame costs nothing and is decisive. --- docs/re/mission-freeze-heap-exhaustion.md | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/re/mission-freeze-heap-exhaustion.md 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.