From bfd4d55ce9ae554218b9823b6ba09833dbd8b764 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 08:10:56 +0000 Subject: [PATCH] re: two wait object types, and the read validates itself Re-extracting the same gdb capture per thread rather than by grep qualifies the previous entry. Of eighteen threads whose frame 3 is XObject::Wait, eight have [rbx] equal to the XEvent vtable plus sixteen, two equal to the XSemaphore vtable plus sixteen, and eight hold a pointer into the mmap region that is not a vtable at all. So the waits are on two distinct kernel types, XEvent and XSemaphore, and the earlier claim that the object is an XEvent was right for the majority but not the whole picture. The eight non-vtable readings are the method checking itself rather than a failure. A polymorphic object's first word is always a vtable pointer, so those values are simply not this: rbx could not be restored for those frames and the unwind returned whatever the register held. A reading counts only if [rbx] lands in the binary's vtable range and resolves to a "vtable for" symbol; anything else is discarded rather than interpreted. Ten of eighteen resolve and the rest are honestly unknown. That also settles the previous entry's worry that the 0x7ffc and 0x7ffd addresses looked like stack. They are the shared mmap region, which holds thread stacks and large allocations alike, so the vtable check rather than the address range is what separates an object from a stack slot. The follow-up run that would have added /proc//maps classification and a wider object dump never booted -- EMULATOR GONE at 0s, skip_intro exit 4 -- most likely a stale emulator or lockfile from the preceding gdb session, whose process tree is parented differently and escaped the usual cleanup. So the map classification, the multi-word object dump and the frozen-state capture are all still unrun. --- docs/re/BACKLOG.md | 14 +++++++++ docs/re/mission-freeze-resume-spin.md | 45 +++++++++++++++++++++++++++ tools/re-capture/freeze_waitobj.sh | 8 ++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 22e53a3..ed53097 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -732,6 +732,20 @@ search cannot find a *schedule*. like **host stack**, not heap, so either xenia places them unusually or `rbx` isn't `Wait`'s `this` after unwind. **Check: dump a few words at `$rbx` — XEvent-like (vtable, KernelState*, handle) vs saved registers.** +* 🟡 **(2026-08-25) Refined: TWO wait types, and the read self-checks.** + Re-extracting per thread (not by grep): of **18** `XObject::Wait` frames, **8** + have `[rbx]` = `vtable for xe::kernel::XEvent`+16, **2** = `vtable for + xe::kernel::XSemaphore`+16, and **8** hold a non-vtable mmap pointer. ⇒ waits + are on **XEvent and XSemaphore**; "it's an XEvent" was the majority, not the + whole picture. ✅ **The 8 misses are the method validating itself** — a + polymorphic object's first word is always a vtable, so a value only counts if + `[rbx]` resolves to a `vtable for …` symbol; `rbx` simply wasn't restorable for + those frames. That also dissolves the "looks like stack" worry: `0x7ffc…` is the + shared mmap region (stacks *and* big allocations), and the **vtable check**, not + the address range, is the discriminator. 🔴 The follow-up run adding + `/proc/maps` classification + `x/8gx` **never booted** (`EMULATOR GONE at 0s`, + stale emulator/lock from the prior gdb session), so that check and the **frozen** + capture are still unrun. * ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**; it rested on an untested assumption that a turn is one shell call. 595 s shell cap − ~220 s boot (a ~190 s title movie that cannot be tapped through) − ~25 s startup = **~350 s observation ≈ 193 game-seconds**. diff --git a/docs/re/mission-freeze-resume-spin.md b/docs/re/mission-freeze-resume-spin.md index 8208ec5..7ccdcfd 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -821,3 +821,48 @@ recorded rather than explained away. against the object pointer recovered from the shim frame above, or dump a few words at `$rbx` and see whether they look like an `XEvent` (a vtable, then a `KernelState*`, then handle/type fields) or like saved registers. + +## 🟡 2026-08-25 — refined: two object types, and the read is self-checking + +Re-extracting the same capture per thread rather than by grep changes the +picture, and qualifies the previous entry. + +Of **18** threads whose frame 3 is `XObject::Wait`: + +| `[rbx]` | count | resolves to | +|---|---|---| +| `0x5555562db8f0` | **8** | `vtable for xe::kernel::XEvent` + 16 | +| `0x5555562dbb40` | **2** | `vtable for xe::kernel::XSemaphore` + 16 | +| a pointer into the mmap region | **8** | not a vtable — see below | + +So the waits are on **`XEvent`** and **`XSemaphore`** objects, two distinct +kernel types, and the earlier "the object is an XEvent" was right for the +majority but not the whole picture. + +## ✅ The 8 non-vtable readings are the method checking itself + +For eight threads `[rbx]` is something like `0x7ffd08bcdeb0` — inside the +mmap region, **not** in the binary's `.data` where vtables live. A polymorphic +object's first word is always a vtable pointer, so those reads are simply **not +`this`**: `rbx` could not be restored for that thread's frame, and the unwind +handed back whatever the register happened to hold. + +That is a useful property rather than a flaw. **The read validates itself:** a +value is a genuine object pointer only if `[rbx]` lands in the binary's vtable +range and resolves to a `vtable for …` symbol. Anything else is discarded rather +than interpreted. Ten of eighteen resolve; the rest are honestly unknown. + +This also answers the previous entry's worry about the `0x7ffc…`/`0x7ffd…` +addresses looking like stack. They are the shared mmap region, which holds thread +stacks *and* large allocations, and the vtable check — not the address range — is +what separates an object from a stack slot. + +## 🔴 The follow-up run never booted + +The run that would have added `/proc//maps` classification and a wider +`x/8gx` dump died immediately: `EMULATOR GONE at 0s (before the window appeared)`, +`BOOT FAILED (skip_intro exit 4)`. Most likely a stale emulator or lockfile from +the preceding gdb session — the gdb wrapper leaves a differently-parented process +tree and the usual cleanup did not catch it. **So the map classification and the +multi-word object dump are still unrun**, and the frozen-state capture still has +not been taken. diff --git a/tools/re-capture/freeze_waitobj.sh b/tools/re-capture/freeze_waitobj.sh index 2075cfb..90652f8 100755 --- a/tools/re-capture/freeze_waitobj.sh +++ b/tools/re-capture/freeze_waitobj.sh @@ -50,12 +50,18 @@ for line in txt.splitlines(): print('thread %s' % cur) print('frame 3') print('info registers rbx') - print('x/1gx $rbx') + print('x/8gx $rbx') # vtable, KernelState*, fields -- or saved regs cur=None print(r'echo === END OBJ ===\n') print('continue') PY sleep 12 +# Is $rbx in a STACK mapping or an anonymous/heap one? 0x7ffc.. is the shared +# mmap region on x86-64: thread stacks live there, but so do large allocations, +# so the address range alone cannot tell them apart. /proc//maps can. +epid=$(pgrep -x xenia_canary | head -1) +[ -n "$epid" ] && cp "/proc/$epid/maps" /tmp/fz-maps.txt 2>/dev/null && \ + echo "--- saved /proc/$epid/maps ($(wc -l < /tmp/fz-maps.txt) mappings)" screenshot /tmp/fz-screen.png >/dev/null 2>&1 echo "--- screen at capture time:" python3 "$SD/screen_id.py" /tmp/fz-screen.png 2>/dev/null | head -2