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.