Source changes (dormant parity infra, retained from iterate 2.AI/2.AO): - xenia-kernel/exports.rs: nt_create_event manual_reset polarity + related event wiring - xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps (.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as regenerable local artifacts — see memory + HANDOFF for the running findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
AUDIT-068 Session 4 — writer identified (guest PPC code)
Date: 2026-05-20
Headline
Writer found. The host-side write of 0x8200A1E8 at [0xBCE251C0] is performed
by JIT-emitted guest PPC code, NOT host C++ code. Reading-error #36 (POD
struct-copy bypass) — registered in Sessions 2 and 3 as the explanation for the
host-side surface gap — is partially superseded: the gap is real for host
C++ writes, but the actual writer of THIS particular vptr install is on the
guest side. AUDIT-067 (which hooks all 16 PPC store opcodes at JIT-emit time)
caught it on the first try, once the correct target value 0x8200A1E8 was
configured (per the Session 3 correction; AUDIT-067's prior runs watched the
wrong value 0x8200A208).
No new instrumentation was needed. Session 4 used the existing AUDIT-067 machinery + Session 3's AUDIT-068 read-probe to cross-validate.
Writer PC and ctor chain
The ANON_Class_713383D7 instance is constructed via a three-level inheritance ctor chain, fully on the guest PPC side. Each ctor writes the next vtable down to slot 0:
sub_824FECE0 (deepest base ctor)
├─ stw r31, 4(r31) ; *(this+4) = this ← self-pointer
├─ stw r31, 8(r31) ; *(this+8) = this ← self-pointer
├─ stw r11=1, 12(r31) ; *(this+12) = 1 (refcount?)
└─ bl 0x8284DD1C ; sub-helper on &this[+16]
↑ called from
sub_825065E8 (intermediate base ctor — writes vtable 0x8200A908)
├─ bl sub_824FECE0 ; chain to deepest base
├─ lis r11, 0x8201; subi r11, 22264 → r11 = 0x8200A908
├─ stw r11, 0(r31) ; *(this) = 0x8200A908
└─ bl 0x825051D8 ; sub-helper init of fields
↑ called from
sub_824FD240 (most-derived ctor — writes vtable 0x8200A1E8)
├─ bl sub_825065E8 ; chain to intermediate base
├─ lis r11, 0x8201; subi r11, 24088 → r11 = 0x8200A1E8
└─ stw r11, 0(r31) ; *(this) = 0x8200A1E8 ← THE INSTALL
The doubly-linked list head sentinel pattern observed by Session 3's read
probe ({vptr, self, self} at offsets {0, +4, +8}) is now fully explained:
- Offsets +4 and +8 are written by the deepest base ctor (
sub_824FECE0at PCs0x824FECFCand0x824FED04) as*(this+4) = this; *(this+8) = this. This is the LIST_ENTRY head sentinel. - Offset 0 is overwritten three times in rapid succession by the inheritance
chain — landing on
0x8200A1E8aftersub_824FD240completes. The read probe (1ms poll period) only ever sees the final value.
All three writes happen on the same 1ms poll tick from the read probe's
perspective, which is why the install LOOKS like a 12-byte POD struct copy. It
is actually 3 separate ctors writing 4 PPC stw instructions (one vtable
slot, three list-init slots, plus a refcount byte that the read probe
neighbor at 0xBCE251CC would have detected). The neighbor at 0xBCE251BC
(-4) DOES NOT change because the ctor only writes at offsets >= 0.
Capture evidence
Run 11 — AUDIT-067 with corrected value 0x8200A1E8
Cmdline: --audit_67_value_watch=0x8200A1E8 --audit_68_host_mem_read_probe=0xBCE251C0:4:1000000 --audit_61_branch_probe_pcs=0x825070F0 --mute=true.
The very first PPC store of 0x8200A1E8 hit:
host_ns=10019392400 CHANGE 0xBCE251C0: 0xBCE25640 → 0x8200A908 (read probe — intermediate base ctor)
host_ns=10021528400 CHANGE 0xBCE251C0: 0x8200A908 → 0x8200A1E8 (read probe — most-derived ctor)
AUDIT-067-VAL pc=824FD264 lr=824FD258 val=8200A1E8 dst=BCE251C0
r3=BCE251C0 r4=00000002 r5=00000020 r6=03A72280
r31=BCE251C0 tid=6
AUDIT-061-BR pc=825070F0 lr=824F7B24 r3=BCE251C0 tid=6 (slot-1 dispatch fires
immediately after)
The intermediate-base vtable write at PC 0x8250660C (value 0x8200A908)
was NOT in this run's AUDIT-067 watch list (only 0x8200A1E8 was), so only
the most-derived hit is logged. Run 12 confirms.
Run 12 — AUDIT-067 with both vtable values + 3-slot read probe
Cmdline: --audit_67_value_watch=0x8200A1E8,0x8200A908,0xBCE251C0 --audit_68_host_mem_read_probe=0xBCE251C0:4:1000000,0xBCE251C4:4:1000000,0xBCE251C8:4:1000000 --audit_61_branch_probe_pcs=0x825070F0 --mute=true.
Captures the full ctor chain on a different cold-trajectory (instance at
0xBCE25340 this time — arena-drift sister of Run 11's 0xBCE251C0):
AUDIT-067-VAL pc=8250660C lr=82506600 val=8200A908 dst=BCE25340 (intermediate base ctor write)
AUDIT-067-VAL pc=824FD264 lr=824FD258 val=8200A1E8 dst=BCE25340 (most-derived ctor write)
AUDIT-061-BR pc=825070F0 lr=824F7B24 r3=BCE25340 (slot-1 dispatch)
Both runs reproduce: the PC pair {0x8250660C, 0x824FD264} is invariant
across cold runs. The instance address VARIES (arena drift), but the writer
PCs do not.
Why earlier sessions missed this
Sessions 1+2
Hooked xe::store_and_swap<T>, xe::store<T>, Memory::Zero/Fill/Copy,
xe::endian_store::set(), Memory::Copy byte-scan, 4 XEX-loader memcpy
sites. These are HOST C++ write paths to guest memory. The JIT does NOT use
them — JIT-emitted PPC stores compile down to direct x64 mov instructions
operating on virtual_membase_ + va, with inline byte-swap intrinsics
(bswap / pshufb). They bypass every xe::store* template.
Reading-error #35 (Session 1: "hook-surface incompleteness") was right to the extent that the surfaces don't cover all host-side write paths — but this writer was never on the host side at all.
Session 3
Read probe captured the install epoch (~9.4-9.6s host_ns) and the neighbor pattern (3 simultaneous writes within 1ms). The "POD struct copy bypass" hypothesis (reading-error #36) was a reasonable explanation under the constraint "host-write surfaces miss the install", but the actual cause is that the writes come from the JIT not from host code at all.
AUDIT-067 (prior to Session 4)
Watched value 0x8200A208. The CORRECT vptr value is 0x8200A1E8 (per
Session 3's correction). AUDIT-067 was hooked into every PPC store opcode and
would have caught the install on the first run if it had watched the right
value. Session 4 re-ran it with the corrected value and caught the writer.
ours-side cross-reference
sub_824FD240 is GUEST PPC code present in the Sylpheed XEX. Both engines'
JIT compiles and runs the same machine code given the same inputs. There is
no host-side analog in xenia-rs/crates/xenia-kernel/ — and there shouldn't
be: this isn't a kernel handler, it's a game's own class constructor.
Per xenia-rs/docs/functions/sub_824F7800.md:
AUDIT-064 ours
--ctor-probe=0x824F7800-n 500M: 0 fires.The chain runs downstream of
sub_822F1AA8's vtable[0] dispatch throughsub_82173990— which waits on tid=13 — so ours never reaches it because tid=13 is blocked on the AUDIT-049 wedge.
sub_824FD240 is reached via:
sub_824F8398
→ sub_824F7CD0
→ sub_824F7800
→ sub_824FD240 (call at PC 0x824F7838) ← THE WRITER
→ ... bctrl at PC 0x824F7B20 dispatches sub_825070F0
In ours, the entire call chain above sub_824F7800 fires 0 times because
the AUDIT-049 wedge blocks tid=13 upstream. Therefore the ANON_Class_713383D7
instance is never constructed, the vtable 0x8200A1E8 is never installed,
and the bctrl at 0x824F7B20 never dispatches sub_825070F0.
This is consistent with all prior phase audits. Session 4 confirms the existing diagnosis: the divergence root is upstream at tid=13, not at the ANON_Class ctor or the worker dispatch.
Static-DB cross-check
| PC | Function | Notes |
|---|---|---|
0x824FECE0 |
sub_824FECE0 (deepest base ctor) |
Writes self-pointers at +4/+8/+12; calls helper at 0x8284DD1C |
0x824FECFC |
inside sub_824FECE0 |
stw r31, 4(r31) — flink_ptr write |
0x824FED04 |
inside sub_824FECE0 |
stw r31, 8(r31) — blink_ptr write |
0x825065E8 |
sub_825065E8 (intermediate base ctor) |
Calls deepest; writes vtable 0x8200A908 |
0x8250660C |
inside sub_825065E8 |
stw r11, 0(r31) — vtable 0x8200A908 write |
0x825051D8 |
called by intermediate base | Sub-helper initializing many +0xXX member fields |
0x824FD240 |
sub_824FD240 (most-derived ctor) |
Calls intermediate base; writes vtable 0x8200A1E8 |
0x824FD264 |
inside sub_824FD240 |
stw r11, 0(r31) — vtable 0x8200A1E8 write — THE INSTALL |
0x824F7800 |
sub_824F7800 |
Allocates instance at +0x38 via sub_824FD230/sub_824FD240 |
0x824F7838 |
inside sub_824F7800 |
bl sub_824FD240 — invokes most-derived ctor |
0x824F7B20 |
inside sub_824F7800 |
bctrl — dispatches sub_825070F0 via vtable slot 1 |
0x825070F0 |
sub_825070F0 (slot-1 method) |
Worker fan-out target — AUDIT-067/061's original lookup |
Static caller chain into sub_824FD240:
- Single static caller:
0x824F7838insidesub_824F7800. - The 4-fn dispatch ladder above (
824F8398 → 824F7CD0 → 824F7800 → bctrl → 825070F0) was already classified by AUDIT-064.
ε-constraint validation
Session 3's install epoch bound was host_ns ∈ [9.4, 9.6] s. Run 11
observed install at host_ns=10.019s; Run 12 captured the intermediate base
ctor write at host_ns ≈ 10s (read probe transition timestamps not
explicitly logged in Run 12 grep output, but within boot's normal jitter
window). The earlier session's ±200ms estimate was off — actual jitter is
closer to ±500ms cold-to-cold. Update: epoch is host_ns ∈ [9.4, 10.1] s.
LOC added (Session 4)
Zero canary LOC added. All Session 4 work used existing AUDIT-067
(JIT-emit value watch in ppc_hir_builder.cc + ppc_emit_memory.cc) and
Session 3's audit_68_host_mem_read_probe cvar machinery (read-mode probe
thread in audit_68_host_mem_watch_base.cc).
Cumulative across Sessions 1+2+3 (canary): ~520 LOC additive, all cvar-gated default-off, retained in tree. Session 4 adds no LOC — the writer was identifiable by re-running existing instrumentation with the corrected target value.
Cascade outcome (Session 4)
- A: identify writer PC — PASS (
sub_824FD240+0x24at PC0x824FD264; most-derived ctor of the inheritance chain). - B: identify caller chain — PASS (
sub_824F7800+0x38→sub_824FD240; matches AUDIT-064's previously-known 4-fn dispatch ladder). - C: identify ours-side analog presence — PASS (no host analog needed; guest PPC code; ours's JIT would execute the same code if reached, but the call chain is unreachable due to tid=13 AUDIT-049 wedge).
- D: reading-error class registration — PASS (see below; #36 re-scoped).
Net 4/4 wins (no in-progress items). Session 4 closes AUDIT-068.
Reading-error class re-scoping
#36 (POD-struct copy-assignment bypass) — registered Sessions 2+3 as the explanation for the host-side surface gap. Session 4 finding: this writer is NOT host C++; it is JIT-emitted PPC code. The class #36 framing remains valid in principle (host C++ POD copy IS a real bypass class, demonstrated by Session 2's reading-error #35 sanity), but it does NOT apply to THIS investigation. Updated rule:
Before adding new host-side write hooks, always check whether the writer could be GUEST CODE running under the JIT. AUDIT-067 (JIT-store value watch) is the cheaper first check. If AUDIT-067 with the correct target value still produces 0 hits, only THEN escalate to host-side surface hooks. The reverse order (Session 1+2's host-first approach) wastes instrumentation budget when the writer turns out to be guest-side.
Secondary rule: always cross-check the configured target value against the
read probe's observed values. Session 1+2+3+AUDIT-067 all watched the wrong
value (0x8200A208) because that was AUDIT-058's quoted value, which was
actually the address of slot-1-WITHIN-the-vtable, not the vtable base. The
read probe directly observed the correct value 0x8200A1E8 in Session 3 —
Session 4 simply propagated that correction to AUDIT-067.
Artifacts (this dir)
run11-audit067-corrected-value.log— AUDIT-067 with value0x8200A1E8; 4 hits (1 install + 3 sibling instances in worker threads).run12-full-ctor-chain.log— AUDIT-067 with full ctor chain (vtable values0x8200A1E8and0x8200A908+ self-pointer0xBCE251C0) and 3-slot read probe; captures all 5 writer-related events on tid=6.writer-report-v4.md— this file.
Discipline observed
- xenia-rs HEAD
e6d43a23ac393004d2e5adf2f0395fd0b5e6448bUNCHANGED ✓ (verified: sha256 ofgit diff HEADat session start =ed30fd526643918f67311caff0a10d1346d73fd0c0323e02477883cf5ff20357; same at session end). --mute=trueon every run ✓- Cold-protocol: cache wipe + restore from
/tmp/canary-cache-bak-audit-068at session end ✓ (backup re-created at session start from current cache; prior session's backup was missing). - Canary tree: no new instrumentation added (zero LOC delta). All Sessions 1-3 instrumentation retained as-is (cvar-gated default-off).
- No destructive shortcuts ✓.
AUDIT-068 closure
AUDIT-068 is CLOSED. The host-side writer of 0x8200A1E8 at
[0xBCE251C0] is conclusively identified as guest PPC code at
sub_824FD240+0x24 (PC 0x824FD264), the most-derived constructor of the
ANON_Class_713383D7 inheritance chain. The intermediate base ctor at
sub_825065E8+0x24 (PC 0x8250660C) writes the intermediate vtable
0x8200A908. The deepest base ctor at sub_824FECE0 writes the
doubly-linked-list head sentinel (self-pointer writes at offsets +4 and +8).
The Phase-NonMatch divergence root remains the upstream tid=13 AUDIT-049
wedge, not the ctor or vtable. ours never reaches the calling code, so
the instance is never constructed and sub_825070F0 never dispatches. No
host-side analog is needed because the writer is part of the game's own
code.
Recommended next steps (NOT Session 5 of AUDIT-068)
Move investigation upstream to the AUDIT-049 / Phase-W wedge at tid=13. That is where ours and canary actually diverge; the ANON_Class ctor and sub_825070F0 are downstream symptoms.
- Re-open the tid=13 wedge analysis under a new audit number.
- Cross-reference
xenia-rs/audit-runs/phase-w-wedge-reattack/current-state.mdfor the most recent state.