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:
sylph-decoder
2026-08-29 22:17:45 +00:00
parent 159778faf1
commit 6d7bc87b0f
3 changed files with 125 additions and 0 deletions

View 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.