diff --git a/docs/re/mission-freeze-heap-exhaustion.md b/docs/re/mission-freeze-heap-exhaustion.md index eee66eb..432f782 100644 --- a/docs/re/mission-freeze-heap-exhaustion.md +++ b/docs/re/mission-freeze-heap-exhaustion.md @@ -809,3 +809,73 @@ it just is not established that the guest is *in* it. function* it belongs to is not. The way to get that honestly is a host→guest code mapping from Xenia itself rather than inference from a stale context — its code cache knows the answer, and dumping that mapping is the next step. + +--- + +# ✅✅ The guest PC, proven — an infinite copy loop in `sub_82457780` + +**2026-08-26.** The previous section withdrew this localisation because it rested +on a stale `PPCContext`. It is now **re-established by a sound method**, and the +withdrawal was right to demand one. + +## The method: Xenia's own JIT annotations + +`emit_source_annotations` (a `CPU` cvar, off by default) makes `MarkSourceOffset` +emit, at **every guest instruction boundary**: + + nop; nop; mov eax, ; nop; nop -> 90 90 B8 xx xx xx xx 90 90 + +So the guest PC is readable straight out of the code bytes around `rip` — no +context, no offsets, no inference. Dumping backwards from `rip` gives a clean run +of them: + + ... B8 74 78 45 82 ... -> guest 0x82457874 + ... B8 78 78 45 82 ... -> guest 0x82457878 + ... B8 7C 78 45 82 ... -> guest 0x8245787C + ... B8 80 78 45 82 ... -> guest 0x82457880 + ... B8 84 78 45 82 ... -> guest 0x82457884 + +and the annotation **immediately before `rip`** is `0x824578A0`. + +**Guest PC = `0x824578A0` = `sth r6, 0(r9)`**, inside `sub_82457780` — the exact +instruction predicted from the disassembly, reached independently. + +## The loop, and why it cannot end + + 82457868 mr r11, r7 ; r11 = start + 8245786c cmplw cr6, r11, r31 + 82457870 beq cr6, 0x824578B4 ; skip entirely if start == end + 8245787c mr r10, r11 ; <== back-edge target + 82457884 addi r11, r11, 8 ; advance by 8 + 8245788c cmplw cr6, r11, r31 + 82457890 lhz r6, 0(r10) ; copy four halfwords + 824578a0 sth r6, 0(r9) ; <== the guest PC + 824578b0 bne cr6, 0x8245787C ; continue while r11 != r31 + +The termination test is **`bne` — exact inequality**, on a pointer advancing in +steps of **8**. If `r31 − r7` is not a positive multiple of 8, the comparison +never becomes equal and **the loop never exits**. + +## ✅ And the thread really is running + +Measured with a *safe* `/proc` parse (see the trap below): over 4 seconds, + +| thread | CPU | +|---|---| +| **`Main XThread`** | **890 ms** | +| `xenia_canary` | 860 ms | +| `XThreadD55FF6C0` | 690 ms | +| `llvmpipe-4` | 500 ms | + +`Main XThread` is the **top consumer**, and the process burns ~238 % CPU overall. +So this is a spin, not a block — which is what an unterminatable loop looks like. + +⚠️ **A bad metric on the way**: I tried to show the loop "marching" through memory +by watching the *last non-zero byte* above `0x70200000`. It never moved — but that +statistic saturates in a region that is already written, so it could not have +moved. It is not evidence either way, and it briefly looked like a refutation. + +❔ Still open, and now narrow: **what makes `r31 − r7` non-congruent to 0 mod 8.** +`r7` is loaded from `0(r30)` and `r31` is the container's end pointer, with a +fresh buffer from `0x824F7240` in `r3` — so the next step is to read those three +values at the moment of the freeze, from *host* registers rather than the context.