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>
294 lines
14 KiB
Markdown
294 lines
14 KiB
Markdown
# 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_824FECE0`
|
|
at PCs `0x824FECFC` and `0x824FED04`) 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 `0x8200A1E8` after `sub_824FD240` completes. 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 through
|
|
> `sub_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: `0x824F7838` inside `sub_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+0x24` at PC `0x824FD264`;
|
|
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 value
|
|
`0x8200A1E8`; 4 hits (1 install + 3 sibling instances in worker threads).
|
|
- `run12-full-ctor-chain.log` — AUDIT-067 with full ctor chain
|
|
(vtable values `0x8200A1E8` and `0x8200A908` + self-pointer
|
|
`0xBCE251C0`) 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 `e6d43a23ac393004d2e5adf2f0395fd0b5e6448b` UNCHANGED ✓
|
|
(verified: sha256 of `git diff HEAD` at session start =
|
|
`ed30fd526643918f67311caff0a10d1346d73fd0c0323e02477883cf5ff20357`; same
|
|
at session end).
|
|
- `--mute=true` on every run ✓
|
|
- Cold-protocol: cache wipe + restore from `/tmp/canary-cache-bak-audit-068`
|
|
at 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.md`
|
|
for the most recent state.
|