re: diagnose the A-press fault -- a wild pointer, and a third failure mode
The blocker on all menu-side dynamic RE in this container, measured over four runs and traced to an instruction. A single A press on the title produces a Xenia CRASH DUMP with PC 0x824578A0 and "Access Violation: write at 0x00000001701D0000", repeating 32 356 times and writing 326 MB of register dump in about ten seconds. Four A runs faulted; four no-input runs in the same sessions completed. REFUTED, my own hypothesis: it is not an unimplemented instruction. The config carries break_on_unimplemented_instructions = true and Xenia's own message reads "to skip, disable break_on_unimplemented_instructions", so the flag looked like the fix. Booting with it false faults identically, and no "Unimplemented instr" line is ever logged on stdout or stderr in any run -- and since that path emits its XELOGE BEFORE the guarded DebugBreak, the absence rules the mechanism out rather than leaving it open. The dump comes from Emulator::ExceptionCallback, a genuine guest exception. The instruction, read from the image rather than the database: b0c90000 is sth r6, 0(r9), the first of four halfword stores at offsets 0/2/4/6 through r9 inside a bne- loop -- code filling an array of 8-byte records with four u16 fields each. So r9 is a wild pointer, and 0x1701D0000 is above 4 GB, outside the guest's 32-bit address space entirely: not a null dereference and not a small overrun, but a base that was never a guest address. The database agrees on the containing function, sub_82457780 at +0x120. It is a THIRD failure mode. Not the cache-flush crash at 0x82307128, and not the loader stall documented in canary-scripted-input-traps.md, which logs ZERO crash dumps. Unlike the stall it reproduced 4 of 4, so that page's "retry whole boots" does not obviously apply. It does not explain how Q4 and Q5 pressed A successfully; what differs between those runs and these has not been found. frame_clock.sh's 300 MB guard killed the run as designed -- the session log's "EMULATOR GONE at 56s" is the guard, not the crash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
91
docs/re/structures/title-a-press-fault.md
Normal file
91
docs/re/structures/title-a-press-fault.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Pressing Ⓐ on the title faults the guest — diagnosed, and it is a third failure
|
||||
|
||||
**Classification: measured.** Xenia Canary, 2026-08-29, four runs. This is the
|
||||
blocker that gates every menu-side dynamic question in this container.
|
||||
|
||||
## What happens
|
||||
|
||||
A single Ⓐ press on the title screen produces a Xenia **`==== CRASH DUMP ====`**:
|
||||
|
||||
```
|
||||
PC: 0x824578A0
|
||||
Access Violation: write at 0x00000001701D0000
|
||||
```
|
||||
|
||||
It repeats **32 356 times** in one run, writing **326 MB** of register dump to
|
||||
stdout in roughly ten seconds. Four runs that pressed Ⓐ on the title all faulted;
|
||||
four runs in the same sessions that pressed nothing all completed normally.
|
||||
|
||||
## 🔴 Refuted: my own hypothesis, that it was an unimplemented instruction
|
||||
|
||||
The config dump carries `break_on_unimplemented_instructions = true`, and Xenia's
|
||||
own message for that path reads *"report the game to Xenia developers; to skip,
|
||||
disable break_on_unimplemented_instructions"* — so the flag looked like the fix.
|
||||
|
||||
**It is not.** Booting with `--break_on_unimplemented_instructions=false` faulted
|
||||
identically, and **no `Unimplemented instr` line is ever logged**, on stdout or
|
||||
stderr, in any run. That path emits its `XELOGE` *before* the guarded
|
||||
`DebugBreak()`, so its absence rules the mechanism out rather than leaving it open.
|
||||
|
||||
The dump comes from `Emulator::ExceptionCallback` (`emulator.cc:1468`), which fires
|
||||
on a **genuine guest exception** — an access violation or illegal instruction
|
||||
inside guest code — not on a translation failure.
|
||||
|
||||
## The faulting instruction, read from the image
|
||||
|
||||
`sylpheed.db` puts the PC inside `sub_82457780` (0x82457780…0x82457958, non-leaf,
|
||||
frame 144), +0x120 in. The image is primary and gives the instruction itself:
|
||||
|
||||
| address | word | instruction |
|
||||
|---|---|---|
|
||||
| **0x824578A0** | `b0c90000` | **`sth r6, 0(r9)`** ← faults |
|
||||
| 0x824578A4 | `b0a90002` | `sth r5, 2(r9)` |
|
||||
| 0x824578A8 | `b0890004` | `sth r4, 4(r9)` |
|
||||
| 0x824578AC | `b1490006` | `sth r10, 6(r9)` |
|
||||
| 0x824578B0 | `409affcc` | `bne-` — loops back |
|
||||
|
||||
Four consecutive halfword stores at offsets 0/2/4/6 through **`r9`**, inside a
|
||||
loop: the code is filling an array of 8-byte records with four `u16` fields each.
|
||||
|
||||
**`r9` is a wild pointer.** The faulting address `0x1701D0000` is above 4 GB and so
|
||||
outside the guest's 32-bit address space entirely — not a null dereference and not
|
||||
a small overrun, but a base that was never a guest address.
|
||||
|
||||
## It is a *third* failure mode, not either known one
|
||||
|
||||
| | PC | crash dumps | this |
|
||||
|---|---|---|---|
|
||||
| cache-flush crash ([`title-crash-stl-tree.md`](../title-crash-stl-tree.md)) | `0x82307128` | yes | ❌ different PC |
|
||||
| loader stall ([`canary-scripted-input-traps.md`](../canary-scripted-input-traps.md)) | — | **zero** | ❌ this has 32 356 |
|
||||
| **this** | **`0x824578A0`** | 32 356 | — |
|
||||
|
||||
⚠️ So the advice in the input-traps page — *"retry whole boots"*, because that
|
||||
failure is not deterministic — does not obviously apply: this one reproduced on
|
||||
**4 of 4** attempts.
|
||||
|
||||
⚠️ **And it does not explain the corpus's earlier successes.** Q4 and Q5 measured
|
||||
all five main-menu buttons, and the focus ring was timed on the menu, so Ⓐ worked
|
||||
then. What differs between those runs and these has not been found.
|
||||
|
||||
## The disk hazard, quantified
|
||||
|
||||
A guest fault writes an **unbounded** register dump: 32 register lines, 32 float
|
||||
lines and 128 vector lines per fault, at ~30 MB/s, on a filesystem at 91 %.
|
||||
|
||||
✅ `tools/re-capture/frame_clock.sh`'s size guard killed this run at its 300 MB cap
|
||||
and worked exactly as designed — the session log's `EMULATOR GONE at 56s` is the
|
||||
guard, not the crash. **Any scripted run that presses a button needs it.**
|
||||
|
||||
## What this blocks
|
||||
|
||||
The main-menu sweeps; whether a `.tbm` draws pixels (needs `GP_SAVE_LOAD`,
|
||||
`GP_BUNK` or `GP_DEBRIEFING_PILOTLOG`); `pbafc.prm`'s blend (needs
|
||||
`GP_READY_ROOM`); and any other menu-side question.
|
||||
|
||||
## Where the next probe goes
|
||||
|
||||
At **`sub_82457780`** and what sets `r9`. The function is non-leaf with a 144-byte
|
||||
frame; the loop writes 8-byte records. Whether `r9` comes from an allocation that
|
||||
failed, a table the guest expects the loader to have filled, or a pointer read back
|
||||
from a structure, is the question — and the corpus's note that the *loader thread*
|
||||
does no file I/O in a failed run is suggestive but is a different failure.
|
||||
Reference in New Issue
Block a user