From a153c392f51c839e3aaa8e7ecea2046d7a23bf2d Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 08:04:33 +0000 Subject: [PATCH] re: the waited object reads as an xe::kernel::XEvent Executed the wait-object read on a live run under gdb. For every thread whose frame 3 is XObject::Wait, rbx gives a pointer whose first quadword is 0x5555562db8f0, which is the PIE base plus 0xd878f0, and the symbol table has vtable for xe::kernel::XEvent at 0xd878e0. The stored pointer is the vtable symbol plus sixteen, since offset-to-top and RTTI come first, so it matches exactly. The same vtable appears on every sampled waiting thread while the this pointers differ, meaning many threads waiting on different XEvent instances. The whole chain needs no DWARF and no rebuild, as the static groundwork predicted. Two caveats, both recorded rather than smoothed over. This is a healthy-play snapshot, not the freeze. The capture landed at 195 s of flight with screen_id reporting flight and a non-black mean, because boot under gdb costs about 300 s and the entire experiment has to fit inside one call -- a timeout kills the process group and takes the emulator with it, which lost an earlier attempt outright. So this describes what threads wait on during normal play, which is the control the frozen capture never had, but it is not the frozen case. And the this addresses look like host stack rather than heap. Either xenia places these objects somewhere unusual, or rbx at frame 3 is not Wait's this after the unwind and the vtable match is coincidence. An exact plus-sixteen match on a known symbol is hard to get by accident, but the address range is not what was expected. The settling check is to dump a few words at rbx and see whether they look like an XEvent -- vtable, KernelState pointer, handle and type fields -- or like saved registers. --- docs/re/BACKLOG.md | 13 ++++++ docs/re/mission-freeze-resume-spin.md | 51 ++++++++++++++++++++++ tools/re-capture/freeze_waitobj.sh | 63 +++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100755 tools/re-capture/freeze_waitobj.sh diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 1e6907b0..22e53a3b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -719,6 +719,19 @@ search cannot find a *schedule*. **Revises the previous entry**, which called route 1 "per-frame archaeology" and route 2 (RelWithDebInfo rebuild) the way to make it easy. **Next: execute on a frozen run** — two gdb commands per thread. +* ✅🟡 **(2026-08-25) Wait-object read WORKS: the object is an `xe::kernel::XEvent`.** + Executed under gdb: `frame 3` → `rbx` → `x/1gx $rbx` = **`0x5555562db8f0`** = + PIE base + `0xd878f0` = **`vtable for xe::kernel::XEvent` (+16 for + offset-to-top/RTTI)** — exact. Same vtable on every sampled waiting thread, + different `this` each, so many threads waiting on *different XEvent instances*. + No DWARF, no rebuild, as predicted. 🟡 **Two caveats, both recorded not glossed:** + (1) this is a **healthy-play** snapshot (`screen_id` = `flight`), not the freeze + — boot under gdb costs ~300 s and the whole thing must fit one call (a `timeout` + kills the process group and took the emulator with it once), leaving too little + to reach the ~267 s freeze; (2) the `this` addresses (`0x7ffd…`, `0x7ffc…`) look + 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.** * ~~🚧 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 d512972c..8208ec50 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -770,3 +770,54 @@ oracle stays byte-identical to the binary every other measurement used. Not yet executed on a frozen run — that is the next step, and it is now a small one. + +## ✅ 2026-08-25 — the technique works end to end: the waited object is an `XEvent` + +Executed on a live run under gdb. For every thread whose frame 3 is +`XObject::Wait`: + +``` +(gdb) === T106 === +#3 0x0000555555e4fd80 in xe::kernel::XObject::Wait(...) +(gdb) rbx 0x7ffd097178d0 +(gdb) 0x7ffd097178d0: 0x00005555562db8f0 +``` + +`0x5555562db8f0` minus the PIE base `0x555555554000` is `0xd878f0`, and the +symbol table has: + +``` +0000000000d878e0 D vtable for xe::kernel::XEvent +``` + +The stored pointer is the vtable symbol **+16** — offset-to-top and RTTI come +first — so it matches exactly. **The object being waited on is an +`xe::kernel::XEvent`**, and the same vtable appears for every waiting thread +sampled (T106, T97, T96, T80, …), while the `this` pointers differ, i.e. many +threads waiting on *different `XEvent` instances*. + +The whole chain — `frame 3` → `rbx` → `x/1gx` → symtab — needs no DWARF and no +rebuild, exactly as the static groundwork predicted. + +## 🟡 Two honest caveats + +**This is a healthy-play snapshot, not the freeze.** The capture landed at 195 s +of flight and `screen_id` reported `flight` with `mean=(17.4, 19.1, 25.1)` — not +the black screen. Boot under gdb costs ~300 s and the whole experiment must fit +in one call (a `timeout` kills the process group, taking the emulator with it — +an earlier attempt lost its run that way), which left too little time to reach +the ~267 s freeze. So this says what threads wait on **during normal play**. That +is the control the frozen capture never had, but it is not the frozen case. + +**The `this` addresses look like host stack, which is odd.** `0x7ffd097178d0`, +`0x7ffc8c1c2f10` and the rest sit in the range host thread stacks occupy, not +where a heap-allocated kernel object would. Either xenia places these objects +somewhere unusual, or `rbx` at frame 3 is not `Wait`'s `this` after the unwind +and the vtable match is a coincidence — an exact +16 match on a known symbol is +hard to get by accident, but the address range is not what was expected and is +recorded rather than explained away. + +**The check that would settle it:** read the same value two ways — compare `rbx` +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. diff --git a/tools/re-capture/freeze_waitobj.sh b/tools/re-capture/freeze_waitobj.sh new file mode 100755 index 00000000..2075cfb9 --- /dev/null +++ b/tools/re-capture/freeze_waitobj.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Read WHICH object the frozen guest threads are waiting on. +# +# Groundwork (mission-freeze-resume-spin.md): XObject::Wait keeps `this` in %rbx, +# and .eh_frame lets gdb restore callee-saved registers during the unwind, so no +# DWARF and no rebuild are needed -- select the Wait frame, read rbx, then read +# the vtable pointer at [rbx] and resolve it in the symtab. +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +export XENIA_BIN=/sylph-home/re/bin/gdb-wrap/xenia_canary +SD="$(cd "$(dirname "$0")" && pwd)" +FLY="${1:-240}" +CMD=/tmp/gdb-cmd; OUT=/tmp/gdb-out.log +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +CFG=/tmp/nav-fz.json +for t in 1 2 3; do + python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1 || true; sleep 3 + python3 "$SD/pad.py" clear >/dev/null 2>&1 || true + if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT=1 SYLPH_KEEPOUT=1400 nohup python3 "$SD/pilot.py" "$CFG" "$FLY" \ + /tmp/fz-pilot.log 2>&1 & P=$!; echo "--- pilot flying"; break + fi +done +echo "--- flying ${FLY}s (boot under gdb costs ~300s, so this must leave room" +echo "--- for the gdb step inside one call: timeout kills the whole group)" +python3 -c "import time,sys; time.sleep(int(sys.argv[1]))" "$FLY" +[ -n "${P:-}" ] && kill "$P" 2>/dev/null +pid=$(pgrep -x gdb | head -1) +[ -n "$pid" ] || { echo "no gdb"; exit 2; } +before=$(wc -c < "$OUT") +kill -INT "$pid"; sleep 3 +{ + echo 'echo === HOT THREAD WAIT OBJECTS ===\n' + echo 'thread apply all bt 6' + echo 'echo === END BT ===\n' +} >> "$CMD" +sleep 10 +tail -c +$((before + 1)) "$OUT" > /tmp/fz-bt.txt +# find threads whose frame 3 is XObject::Wait, then read rbx there +python3 - <<'PY' >> "$CMD" +import re +txt=open('/tmp/fz-bt.txt').read() +cur=None +for line in txt.splitlines(): + m=re.match(r'Thread (\d+) ', line) + if m: cur=m.group(1) + if cur and 'XObject::Wait' in line: + print(r'echo === T%s ===\n' % cur) + print('thread %s' % cur) + print('frame 3') + print('info registers rbx') + print('x/1gx $rbx') + cur=None +print(r'echo === END OBJ ===\n') +print('continue') +PY +sleep 12 +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 +tail -c +$((before + 1)) "$OUT" | sed -n '/HOT THREAD WAIT OBJECTS/,$p' | grep -vE "^\s*$" | tail -70 +echo "FREEZE WAITOBJ DONE"