From 9ea0a00d807ec8057ac0265b08b908d433636d0c Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 26 Aug 2026 17:28:31 +0000 Subject: [PATCH] re: locate the stuck thread -- a grow-and-copy in sub_82457780 Ran the freeze under the corpus's gdb wrapper (ptrace_scope is 1, so a debugger must launch rather than attach, and its handle lines are needed because Xenia uses SIGSEGV for guest memory watches). One thread of 80 is in guest code: thread 50, Main XThread, at rip a05be939 in JIT output. All others are in a futex or clock_nanosleep. Xenia's x64 backend keeps PPCContext in rsi, with r[32] at +0x20. The guest GPRs there give r8 = a3ac0000 (the 64 MB buffer from the doubling sequence), r12 = a3ac0a18 inside it, and r13 = 82457864 -- guest code, sub_82457780. That address sits in a grow-and-copy: size = count*8 clamped to 0x1FFFFFFF, a call to 0x824F7240, then a loop copying halfwords eight bytes at a time. The host instruction it is stopped on is that copy's store: mov %r12w,(%rdi,%rax,1) rdi = membase, rax = 0x701d0000 Sampled three times seconds apart with continue in between: rip, r13, r11 and r31 identical every time. No progress. The loop's exit test is beq -- equality, not >= -- so an inconsistent start/end pair never terminates it. Recorded as amber: that is a reading of the disassembly, not a demonstration. Two of my own readings corrected: * "the guest spins at ~400% CPU" was ps's CUMULATIVE AVERAGE since process start, not an instantaneous rate. Per-thread sampling puts Main XThread nowhere near the top. * a SIGSEGV fault storm fitted the constant rip nicely and is refuted: 1500 minor faults in 5 s, zero major. Caveat kept on the page: these are gdb-hosted observations and gdb intercepts every SIGSEGV, so absolute timings are not the ungoverned ones. The freeze is not a gdb artefact -- it reproduces in every non-gdb run. --- docs/re/mission-freeze-heap-exhaustion.md | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/re/mission-freeze-heap-exhaustion.md b/docs/re/mission-freeze-heap-exhaustion.md index 2695c246..de0c0124 100644 --- a/docs/re/mission-freeze-heap-exhaustion.md +++ b/docs/re/mission-freeze-heap-exhaustion.md @@ -673,3 +673,76 @@ two frames rather than one. excluded: the heap failure, the leak, allocation rounding, `MmQueryStatistics`, a heap-size knob, the exception cvar, a kernel-object wait, a build regression, the navigation route, the savegame, and slow shader compilation. + +--- + +# ✅ The stuck thread is located: a grow-and-copy in `sub_82457780` + +**2026-08-26.** Ran the freeze under the corpus's own gdb wrapper +(`/sylph-home/re/bin/gdb-wrap/xenia_canary` — `ptrace_scope` is 1, so a debugger +must *launch* the process, and the wrapper's `handle` lines are required because +Xenia uses SIGSEGV for guest memory watches). + +**One thread of 80 is in guest code.** Every other thread sits in a futex or +`clock_nanosleep`; thread 50, `Main XThread`, is at `rip = 0xa05be939`, inside +JIT-generated code rather than libc. + +## Where, in guest terms + +Xenia's x64 backend keeps the `PPCContext` in **`rsi`** +(`X64Emitter::GetContextReg() { return rsi; }`), with `r[32]` at `+0x20`. Reading +the guest GPRs there: + +| reg | value | | +|---|---|---| +| `r8` | `0xa3ac0000` | the 64 MB buffer from the doubling sequence | +| `r12` | `0xa3ac0a18` | inside that buffer | +| **`r13`** | **`0x82457864`** | **guest code — `sub_82457780`** | + +And `0x82457864` sits in a **grow-and-copy**: a size computed as `count × 8` +(`slwi r3, r27, 3`) clamped against `0x1FFFFFFF`, a call to `0x824F7240`, then a +loop that copies halfwords (`lhz`/`sth`) eight bytes at a time. + +The host instruction it is stopped on is exactly that copy's store: + + => 0xa05be939: mov %r12w,(%rdi,%rax,1) rdi = 0x100000000 (membase) + rax = 0x701d0000 (guest address) + +a **16-bit store** — the JIT's rendering of the loop's `sth`. + +## And it is not making progress + +Sampled three times, seconds apart, with `continue` in between: + + rip=a05be939 r13=82457864 r11=0 r31=701cf898 + rip=a05be939 r13=82457864 r11=0 r31=701cf898 + rip=a05be939 r13=82457864 r11=0 r31=701cf898 + +Identical every time. 🟡 Worth noting the loop's exit test is `beq` — it +terminates only when `r11` becomes **exactly equal** to `r31`, not `>=`. A start +or end pointer that is inconsistent (or not 8-aligned relative to the other) +would never satisfy it. That is a *reading of the disassembly*, not a +demonstration, and `r11 = 0` against `r31 = 0x701cf898` is at least consistent +with it. + +## 🔴 Two of my own readings corrected + +* **"The guest spins at ~400 % CPU"** — wrong. That was `ps`'s **cumulative + average since process start**, not an instantaneous rate. Per-thread sampling + puts `Main XThread` nowhere near the top; the busiest are another guest thread + at 24 % and the `llvmpipe` software rasterizers at ~7 % each. +* **A SIGSEGV fault storm** (Xenia's memory watches retrying forever) fitted the + constant `rip` nicely and is **refuted**: 1 500 minor faults in 5 s, ~300/s, + and zero major faults. + +## ⚠️ Caveat on the method + +These observations are under gdb, which intercepts every SIGSEGV even with +`nostop noprint pass`, so absolute timings here are not the ungoverned ones. The +freeze itself is not a gdb artefact — it reproduces in every non-gdb run — but +"how slow" should be re-measured without it. + +❔ Next: confirm or refute the `beq` reading by watching `r11`/`r31` across a +longer window, and identify what `0x824F7240` returns — if that allocator hands +back a buffer whose end is not `start + count×8`, the loop's equality test is the +freeze.