From ab8a0d9e6d617df7e56bbc2152b04c04b9bb741f Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 18 Aug 2026 21:05:39 +0000 Subject: [PATCH] docs/re: the title-screen crash is an STL map/set erase, 40 s from a cold boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crash PC from the previous iteration resolves, and it names itself: sub_823070B0 references the string 'invalid map/set iterator', builds it with the string helpers and throws it — which is the guest C++ exception (E06D7363) Xenia reports one line earlier. Its node offsets are MSVC's red-black tree node exactly (_Left 0, _Parent 4, _Right 8, _Color 24, _Isnil 25), so this is a std::map/set erase, and the fault is the first dereference after the iterator check. That matters beyond this blocker. The canary handoff's Ready-Room crash is the same shape — a guest STL exception in a cache/save path — and its bisection plan is priced at "one build plus one Ready-Room run" per suspect. If it is the same defect, each suspect now costs 40 seconds, and suspect #1 is already measured: --mem_watch=false removes it. Stated as unresolved rather than guessed: the fault address 0x1_0000000C is a 32-bit value with bit 32 set, which fits BOTH a corrupt guest tree and a stale high word on the emulator side. The measurement that separates them (read the node from guest memory at the throw) is written down rather than assumed. The second, unreproduced crash PC is identified too: an unrolled 4x16-bit copy loop faulting on the STORE, i.e. a bad destination — a different failure. --- docs/re/INDEX.md | 1 + docs/re/canary-scripted-input-traps.md | 15 ++-- docs/re/title-crash-stl-tree.md | 98 ++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 docs/re/title-crash-stl-tree.md diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index e992eb6..49897e0 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -27,6 +27,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes | UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `.t32` sprite has a `.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly | | UI screen paint order | ✅/🟡 | [title paint order](ui-title-paint-order-capture.md) | **Measured from the guest's draw stream** (new Canary hook `log_ui_draws`, F10): the title screen paints 11 draws a frame through one sprite shader, and the submission order is background → `back2` glow → `ptlogo1`+`tm` → `ptlogo2` → `ptcopyright` → the `PRESS Ⓐ BUTTON` plate. That refutes declaration order, reverse order, keyframe-start order and resting-time order, and shows the screen composites **two bundles** (build 7 + the one-element build 2). 🟡 the rule that produces the order is still unknown | | Scripted input / profile traps | ✅/🔴 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Three reasons a scripted run "cannot press Ⓐ": **F10 opens the emulator menu bar**, which makes `IsUIActive()` true and zeroes every guest keystroke; the file-pad is not the problem (it delivers, and Ⓐ skips movies); and the title needs a **signed-in profile**, which the container cannot type into ImGui — hence Canary's new `--create_profile_if_none`. 🔴 Past that, Ⓐ crashes the guest with `--mem_watch=true` and stalls it with `--mem_watch=false` — a cheap second reproduction of the Ready-Room crash, 40 s from a cold boot | +| Title-screen guest crash | ✅/🟡 | [title-crash-stl-tree](title-crash-stl-tree.md) | `0x82307128` is inside an MSVC `std::map`/`set` **erase** — the function builds and throws `'invalid map/set iterator'`, and its node offsets (`_Left` 0, `_Right` 8, `_Isnil` 25) are the MSVC red-black node. It crashes on the first dereference after that check, at `0x1_0000000C` (a 32-bit value with bit 32 set). Two runs, ~40 s from a cold boot with `--mem_watch=true`; **none** with `--mem_watch=false` — so probably the Ready-Room crash, reproducible in seconds instead of a mission | | Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet ✅ *0 locked / 2 developable / 4 developed* (only the `4`s are stored — `2` is re-derived at load). **Saves can also be written back**: three derived header fields (length at `+0x30`, payload length at `+0x8c`, `adler32` at `+0x8e`) are all that stand between a parse and a hand-written save that the title loads, and [`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the [economy note](arsenal-develop-economy.md) | ## Runtime / dynamic-capture technique diff --git a/docs/re/canary-scripted-input-traps.md b/docs/re/canary-scripted-input-traps.md index f9bf97f..380a7df 100644 --- a/docs/re/canary-scripted-input-traps.md +++ b/docs/re/canary-scripted-input-traps.md @@ -104,11 +104,18 @@ C++ exception in a cache/save path. This is a second, far cheaper reproduction o it: at the **title screen**, ~40 s from a cold boot, instead of in the Ready Room after a mission. -**Next step, for whoever picks this up.** Two threads to pull, in this order: +**The first of those next steps is done**, and it moves the crash out of this +entry into its own: `0x82307128` is inside an MSVC `std::map`/`set` **erase** — +the function carries the string `'invalid map/set iterator'` and the node +offsets to prove it — which makes this a 40-second reproduction of the crash the +Canary handoff blames on the `mem_watch` probe. See +[`title-crash-stl-tree.md`](title-crash-stl-tree.md). -1. `0x82307128` and `0x824578A0` in `xenia-rs/sylpheed.db` (the container has it) - — what function is the title's Ⓐ path in, and what is at `+0xC` of the - structure it dereferences; +**Next step, for whoever picks this up.** What is left: + +1. dump the tree nodes from guest memory at the throw, to separate "the guest's + tree is corrupt" from "the emulator left a stale high word in a register" — + the fault address `0x1_0000000C` is consistent with either; 2. the content exports this title imports — `XamContentCreateEnumerator`, `XamContentGetDeviceState`, `XamUserReadProfileSettings` — logged around the press, to see whether the guest is handed a bad enumerator or a device state diff --git a/docs/re/title-crash-stl-tree.md b/docs/re/title-crash-stl-tree.md new file mode 100644 index 0000000..ad4bbea --- /dev/null +++ b/docs/re/title-crash-stl-tree.md @@ -0,0 +1,98 @@ +# The title-screen crash is an STL `map`/`set` erase on a bad iterator + +**Status:** ✅ `CONFIRMED` for the identification (the guest function is +`std::_Tree::erase`-shaped, and it says so in its own diagnostic string). +🟡 `PROBABLE` that this is the same defect as the Ready-Room crash the Canary +handoff blames on the `mem_watch` probe. ❔ whether the bad pointer originates in +the guest or in the emulator's 64-bit register handling. + +Found while trying to get past the title screen for a second UI screen's paint +order ([`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)). It is +worth a page of its own because it is a **40-second reproduction** of a crash the +project has so far only seen after a whole mission. + +## The reproduction + +Boot Canary on the disc with the default `--mem_watch=true` and a profile on +disc. Roughly 40 s in — while the title screen is up — the guest dies: + +``` +Access Violation: read at 0x000000010000000C +PC: 0x82307128 guest thread 9 +… preceded by HostPathDevice::ResolvePath(\aab216c3\5\c10eae6) + and RtlRaiseException(702DF7F0(E06D7363), ContextArg) + "Guest attempted to throw a C++ exception!" +``` + +Two runs, same PC, same fault address. With `--mem_watch=false` it does not +happen at all. Xenia pauses itself and stacks crash dialogs — 991 in one run. + +## What the code is + +`0x82307128` is inside `sub_823070B0` (`0x823070B0..0x823074D0`, has EH), and the +function identifies itself: it references the string +`'invalid map/set iterator'` at `0x82062A8C`, builds it with the string +helpers at `0x8216E7E8` / `0x8216E5C8`, and throws it through `0x825F23D8`. + +The node layout in the prologue is MSVC's `std::_Tree_node` exactly: + +``` +823070C8 lbz r10, 25(r5) ; iterator->_Ptr->_Isnil (offset 25) +… ; if set -> build the string and THROW +82307124 bl 0x8244E2A8 ; (iterator helper) +82307128 lwz r11, 0(r25) ; node->_Left (offset 0) <-- FAULT +8230713C lwz r27, 8(r25) ; node->_Right (offset 8) +``` + +`_Left` 0, `_Parent` 4, `_Right` 8, `_Color` 24, `_Isnil` 25 — that is the MSVC +red-black tree node, so this is a `std::map`/`std::set` **erase** (it validates +the iterator, then walks the node). The crash is the very first dereference +after the validation. + +## What the fault address says + +The effective address is `r25 + 0`, so `r25 = 0x00000001_0000000C`. That is a +32-bit value `0x0000000C` **with bit 32 set** — a pointer whose high word is +garbage, not a wild 32-bit pointer. Two readings, and this note does not choose +between them: + +* the tree really is corrupt and the guest is erasing through a freed/racing + node — which is what the + [`crash-oracle handoff`](../../xenia-canary/HANDOFF-crash-oracle-2026-07-16.md) + concluded for the Ready-Room crash (a concurrent cache-add landing inside an + unlocked flush iteration), and it names the `mem_watch` polling thread as + suspect #1 for perturbing that timing. The `--mem_watch=false` result here is + consistent with that; +* or a high-word extension bug on the emulator side leaves a stale 1 in the + upper half of a 64-bit register. Canary is the *reference* emulator, so this is + the less likely of the two, but `0x1_0000000C` is exactly the shape such a bug + produces and nothing here rules it out. + +**What would separate them:** dump the tree's root and a few nodes from guest +memory (`tools/re-capture/gmem.py`) at the moment of the throw, and check whether +the parent node's `_Left` field on the guest heap really contains `0x0000000C` — +if the guest memory holds a clean 32-bit value and only the register is wrong, +it is the emulator. + +## The other crash PC, for completeness + +The one unreproduced crash after an Ⓐ press was at `0x824578A0`, in +`sub_82457780` (`0x82457780..0x82457958`, one caller, `sub_82457038`). It is an +unrolled **4 × 16-bit copy loop**: + +``` +82457890 lhz r6, 0(r10) 82457898 lhz r4, 4(r10) +82457894 lhz r5, 2(r10) 8245789C lhz r10, 6(r10) +824578A0 sth r6, 0(r9) <-- FAULT (a STORE, not a load) +``` + +so a bad *destination*, in what looks like a small block copy — a different +failure from the tree erase above, and with one observation it stays at that. + +## Why this matters beyond the blocker + +The Ready-Room crash costs a full mission to reproduce, which is why its +bisection plan in the handoff is written as "one build + one Ready-Room run" per +suspect. If this title-screen crash is the same defect, each suspect costs **40 +seconds** instead, and suspect #1 (`--mem_watch=false`) is already measured here: +it removes the crash.