From abed25fd67dbcb3908739f4ba962e307203f4d09 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 17:50:57 +0000 Subject: [PATCH] re: prove the guest PC -- an infinite copy loop in sub_82457780 The previous section withdrew this localisation for resting on a stale PPCContext. It is now re-established by a sound method. Xenia has a CPU cvar, emit_source_annotations, that makes MarkSourceOffset emit "nop; nop; mov eax,; nop; nop" at EVERY guest instruction boundary -- 90 90 B8 xx xx xx xx 90 90 in the code bytes. So the guest PC reads straight out of memory around rip, with no context and no offset guessing. Dumping backwards from rip gives a clean run of them -- 0x82457874, 78, 7C, 80, 84 -- and the annotation immediately before rip is 0x824578A0. That is `sth r6, 0(r9)` inside sub_82457780: the exact instruction predicted from the disassembly last time, now reached independently. The loop advances r11 by 8 and terminates on `bne cr6` -- exact inequality. If r31 - r7 is not a positive multiple of 8, the comparison never becomes equal and the loop never exits. And the thread is genuinely running: over 4 s, Main XThread consumed 890 ms of CPU, the top of any thread, with the process at ~238%. A spin, not a block. One bad metric recorded: 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 an already-written region, so it could not have moved. Not evidence either way, though it briefly looked like a refutation. Open and now narrow: what makes r31 - r7 non-congruent to 0 mod 8. r7 comes from 0(r30), r31 is the container end, and r3 holds a fresh buffer from 0x824F7240 -- read those three from HOST registers at the freeze. --- docs/re/mission-freeze-heap-exhaustion.md | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) 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.