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>
17 KiB
AUDIT-068 Session 3 — read-mode probe writer report
Date: 2026-05-20
Summary
Session 3 adds a read-mode probe to the AUDIT-068 instrumentation. Instead
of hooking host-side write surfaces (Session 1+2's approach, which produced 0
hits across ~9 surfaces despite the install being real), the probe spawns a
dedicated low-priority polling thread that samples configured guest VAs every
PERIOD_NS and emits AUDIT-068-READ-CHANGE events on transition.
The probe bounded the install epoch for the ANON_Class_713383D7 vptr to
host_ns ≈ 9.412–9.612 s (varies ±200 ms between cold runs) and provided
the first direct evidence that the install is a bulk POD struct copy of a
12-byte {vptr, self_ptr, self_ptr} record into the instance's first three
u32 slots — written simultaneously within the same 1 ms poll interval.
Reading-error class #36 (POD-struct copy-assignment bypass) is now
confirmed in the strongest possible terms: Run 10 enabled BOTH the read
probe AND the full ~9-surface host-write watch simultaneously with the
CORRECT target value 0x8200A1E8, and observed the read probe catch the
install while host-write surfaces produced 0 hits.
A secondary finding overturns part of the AUDIT-067 framing: the actual vptr
value installed is 0x8200A1E8, not 0x8200A208. The number 0x8200A208
is the address of the slot-1 fn pointer WITHIN the vtable (32 bytes into the
vtable). The value stored at [ctx_ptr] is the vtable BASE = 0x8200A1E8.
AUDIT-067 hooked all 16 PPC store opcodes for 0x8200A208 — it should have
also (or instead) watched 0x8200A1E8. This may explain part of why AUDIT-067
also produced 0 hits.
LOC added (Session 3 delta, canary only)
| File | LOC delta | Purpose |
|---|---|---|
src/xenia/cpu/cpu_flags.h |
+7 | New cvar audit_68_host_mem_read_probe declaration. |
src/xenia/cpu/cpu_flags.cc |
+6 | Cvar definition. |
src/xenia/memory.cc |
+18 | Register g_guest_to_host_thunk (wraps Memory::TranslateVirtual) and g_query_protect_thunk (wraps LookupHeap+QueryProtect) inside Memory::Memory(); reset to nullptr in ~Memory(). |
src/xenia/base/audit_68_host_mem_watch_fwd.h |
+17 | GuestToHostThunk + QueryProtectThunk extern decls. |
src/xenia/base/audit_68_host_mem_watch_base.cc |
+~170 | ReadProbe struct + parser (VA:SIZE:PERIOD_NS CSV form) + sample_at() w/ page-protect guard + read_probe_thread_main() polling loop + start_read_probe_thread_if_configured() lazy-start (called from check_host_write_slowpath). |
| Total | ~218 LOC additive | All cvar-gated default-off (empty CSV = thread never spawned). |
Cumulative across Sessions 1+2+3: ~520 LOC.
xenia-rs HEAD e6d43a23ac393004d2e5adf2f0395fd0b5e6448b UNCHANGED.
Cvar format
--audit_68_host_mem_read_probe=VA1:SIZE1:PERIOD1,VA2:SIZE2:PERIOD2,...
Each tuple is VA:SIZE:PERIOD_NS. SIZE ∈ {1, 2, 4, 8}. PERIOD_NS floored at
1 us (1000). Max 8 tuples. Default empty (off).
Lazy-start: the poll thread spawns only on the first call to
check_host_write_slowpath() after Memory::Memory() has registered the
thunks. This reuses the Session 2 static-init gate. The thread is detached
(daemon-style) and polls until process exit.
Captures
All runs cold-boot (cache wipe before each), --mute=true, against the
Sylpheed ISO. 90 s wallclock each.
Run 6 — primary read-probe on 0xBCE25340
Cmdline: --audit_68_host_mem_read_probe=0xBCE25340:4:1000000 --mute=true.
Observations:
host_ns=729615200 INITIAL 0x00000000
host_ns=738072700 CHANGE 0x00000000 → 0xBCE254C0 (arena-local pointer)
host_ns=1537758000 CHANGE 0xBCE254C0 → 0xBCE25640
host_ns=1591760600 CHANGE 0xBCE25640 → 0xBCE25350
host_ns=1592827100 CHANGE 0xBCE25350 → 0xBCE257C0
host_ns=1601443500 CHANGE 0xBCE257C0 → 0x82061050 (looks like XEX vtable)
host_ns=1602506700 CHANGE 0x82061050 → 0x820610E0 (final, stable through 90 s)
Boot reached worker spawn (thid=27/28/29 visible in log tail) — so the
probe was alive for the whole 90 s wallclock; only ~7 changes occurred at
0xBCE25340 in this run, and the value never became 0x8200A208.
This indicated the address 0xBCE25340 cited in AUDIT-058/067 is NOT
deterministic across runs — there's "arena drift" in the 0xBCE25xxx region.
The Phase-NonMatch investigation memo (2026-05-19) already documented this:
canary cold sample saw ctx_ptr=0xBCE251C0 while AUDIT-058 saw 0xBCE25340.
Run 7 — neighbor bisect on 0xBCE25340 ± 4/8
Cmdline: --audit_68_host_mem_read_probe=0xBCE2533C:4:1000000,0xBCE25340:4:1000000,0xBCE25344:4:1000000,0xBCE25348:4:1000000.
host_ns=655976500 INITIAL all four = 0
host_ns=664462100 CHANGE 0xBCE25340: 0 → 0xBCE254C0
host_ns=1374604200 CHANGE 0xBCE25340: 0xBCE254C0 → 0x07C65ADA (3 SIMULTANEOUS)
host_ns=1374604200 CHANGE 0xBCE25344: 0 → 0x001EE000
host_ns=1374604200 CHANGE 0xBCE25348: 0 → 0x0003A313
Key signal: at host_ns=1.374 s, three adjacent u32 slots changed within
the same 1 ms poll interval but the neighbor at 0xBCE2533C did NOT. This is
a clear bulk struct-copy / memcpy footprint — the writer wrote a 12-byte
record starting at 0xBCE25340. The three values {0x07C65ADA, 0x001EE000, 0x0003A313} are NOT the vtable (don't match 0x8200A208/0x8200A1E8); they
look like random-looking data (FNV-style hash, allocation size, refcount?).
This particular write happens to a DIFFERENT object instance reusing the
0xBCE25340 slot, not the ANON_Class instance.
Run 8 — locate the actual ctx_ptr via AUDIT-061 fire
Cmdline: --audit_61_branch_probe_pcs=0x825070F0 --audit_68_host_mem_read_probe=0xBCE25340:4:1000000.
AUDIT-061-BR pc=825070F0 ... r3=BCE251C0 ... fired late in the run. So in
THIS cold trajectory the ANON_Class instance is at 0xBCE251C0, not
0xBCE25340. The probe at 0xBCE25340 was watching the wrong address.
Run 9 — neighbor bisect on the correct ctx_ptr 0xBCE251C0
Cmdline: --audit_61_branch_probe_pcs=0x825070F0 --audit_68_host_mem_read_probe=0xBCE251BC:4:1000000,0xBCE251C0:4:1000000,0xBCE251C4:4:1000000,0xBCE251C8:4:1000000.
host_ns=633560300 INITIAL all four = 0
host_ns=642041900 CHANGE 0xBCE251C0: 0 → 0xBCE25340 (arena ptr)
host_ns=1387443500 CHANGE 0xBCE251C0: 0xBCE25340 → 0xBCE254C0 (2 SIMULTANEOUS)
host_ns=1387443500 CHANGE 0xBCE251C8: 0 → 0x00000148
host_ns=1412116800 CHANGE 0xBCE251C0: 0xBCE254C0 → 0 (2 SIMULTANEOUS clear)
host_ns=1412116800 CHANGE 0xBCE251C8: 0x148 → 0
host_ns=1457544600 CHANGE 0xBCE251C0: 0 → 0xBF80199A (2 SIMULTANEOUS — floats)
host_ns=1457544600 CHANGE 0xBCE251C4: 0 → 0x3F802D83 (= -1.0008, 1.0014)
host_ns=5710239000 CHANGE 0xBCE251C0: 0xBF80199A → 0xBCE25640 (arena ptr)
host_ns=9416025400 CHANGE 0xBCE251C0: 0xBCE25640 → 0x8200A1E8 (3 SIMULTANEOUS — THE INSTALL)
host_ns=9416025400 CHANGE 0xBCE251C4: 0xBCE251C0 → 0xBCE251C0 (self-ptr)
host_ns=9416025400 CHANGE 0xBCE251C8: 0 → 0xBCE251C0 (self-ptr)
AUDIT-061-BR pc=825070F0 r3=BCE251C0 (fire ~25 s wallclock)
The install epoch is host_ns = 9.416025400 s. Three slots written
simultaneously to {vptr=0x8200A1E8, self=0xBCE251C0, self=0xBCE251C0} —
classic struct construction or *ptr = X_FOO{...} POD copy pattern. The
slot at 0xBCE251BC (4 bytes before ctx_ptr) did NOT change, bounding the
write to exactly 12 bytes starting at 0xBCE251C0.
The install is ~966 ms BEFORE the sub_825070F0 fire (~10.4 s host_ns,
matches Phase-NonMatch documented thread.create burst at 10.382 s) and well
within the 60-90 s capture window.
Run 10 — cross-validation: read-probe + host-write watch with correct value
Cmdline: --audit_68_host_mem_watch_values=0x8200A1E8,0x8200A208,0xE8A10082,0x82A10082 --audit_68_host_mem_watch_addrs=0xBCE251C0 --audit_68_host_mem_read_probe=0xBCE251C0:4:1000000 --audit_61_branch_probe_pcs=0x825070F0.
host_ns=9612147300 CHANGE 0xBCE251C0: 0xBCE25640 → 0x8200A1E8 (read probe catches)
AUDIT-061-BR pc=825070F0 r3=BCE251C0 (sub_825070F0 fires)
AUDIT-068-HOST-WRITE: 0 hits (write surfaces miss)
This is the definitive proof:
- The install IS captured by the read probe at host_ns ≈ 9.6 s.
- The corrected value
0x8200A1E8(not0x8200A208) is the actual vptr. - None of the ~9 host-write surfaces hooked in Session 1+2 catches it.
Reading-error class #36 confirmed: the writer uses a path that bypasses
all of xe::store_and_swap<T>, xe::store<T>, Memory::Zero/Fill/Copy,
xe::endian_store::set(), and Memory::Copy byte-scan — most likely a
*reinterpret_cast<X_FOO*>(host_ptr) = X_FOO{...} raw POD struct
copy-assignment OR a direct memcpy(host_ptr_from_TranslateVirtual, &local_struct, sizeof(X_FOO)).
Headline finding
Install epoch: host_ns ≈ 9.4–9.6 s (varies ±200 ms across cold runs). This is ~966 ms before sub_825070F0 fires (~10.4 s host_ns).
Neighbor pattern: 3 simultaneous writes at 0xBCE251C0, +4, +8
within the same 1 ms poll interval — {vptr=0x8200A1E8, self=0xBCE251C0, self=0xBCE251C0}. 0xBCE251BC (-4) does NOT change. This is a 12-byte
POD struct copy.
Implications:
- The write is invisible to all currently-hooked host-write surfaces.
- The value bytes
{0xE8, 0xA1, 0x00, 0x82, 0xC0, 0x51, 0xE2, 0xBC, 0xC0, 0x51, 0xE2, 0xBC}(big-endian guest order) must appear together in some source — either as a constant pre-baked vtable instance pattern that's memcpy'd, or as fields computed by host code and bulk-written. - The fact that the second and third slots are self-pointers (
= ctx_ptr) suggests a doubly-linked-list head node initialization:head.vptr = vtbl; head.next = &head; head.prev = &head;. This is a textbook intrusive list / queue head pattern.
Wallclock relation to AUDIT-067's sub_825070F0 fire
| Event | Host_ns | Wallclock (≈) |
|---|---|---|
| Probe init (first slowpath call) | ~640 ms | ~1.6 s |
| Various pre-install arena reuse of slot | 0.6–5.7 s | 1.6–6.5 s |
Vptr install at 0xBCE251C0 |
9.412–9.612 s | ~10.4–10.6 s |
| Phase-NonMatch documented thread.create burst | 10.382–10.384 s | ~11.3 s |
| sub_825070F0 fire (AUDIT-061-BR captured) | ~10.5 s | ~25 s wallclock (AUDIT-067 quoted) |
The "host_ns ~10.5 s when sub_825070F0 fires" vs "~25 s wallclock" gap is
because host_ns starts when the first AUDIT-068 slowpath call lands (i.e.
when canary's static-init plus Wine startup are done) — Wine's
JIT-warmup/early-boot takes ~15 s before guest PPC code starts. The
ANON_Class install happens ~960 ms before sub_825070F0 dispatch, within the
same "post-DiscImageDevice resolve" boot phase that AUDIT-058 framed.
Session 4 recommendation
Three paths to identifying the writer, ranked by feasibility:
Path 1 (RECOMMENDED) — POD struct-copy hook with NEW ε-constraint
The install epoch (host_ns ≈ 9.4–9.6 s) and the 12-byte simultaneous-write signature (3 u32 slots) narrows the candidate hooks dramatically. Two surgical instrumentation strategies:
(a) Pre-instrument all *reinterpret_cast<X*>(host_ptr) = X{...} sites in
canary. Ripgrep finds them: pattern
\*reinterpret_cast<[A-Z]\w*\*>\([^)]*\)\s*= in src/xenia/kernel/**.cc. A
quick scan of Session 1 inventory listed ~30 such sites, but most are in
kernel-import handlers that fire repeatedly — the ε-constraint of "fires
exactly once at host_ns 9.4–9.6 s on tid=6" lets us bisect.
(b) Wrap xe::SetField() / pointer-typed assignment helpers if any
exist. Otherwise instrument memcpy(host_ptr_from_TranslateVirtual, ...)
patterns directly — there are ~40 such sites across kernel/util/cpu code per
Session 1+2 surveys. The ones NOT already wrapped by Session 2 (xex_module.cc
got 4 sites) are candidates.
LOC budget: ~50-100 additive in canary; default-off cvar
audit_68_pod_copy_watch_addrs (CSV of VA ranges; emits on every memcpy/raw
assign within range).
Path 2 — Guard-page SIGSEGV trap
Use the existing canary ExceptionHandler infrastructure
(src/xenia/base/exception_handler*.cc — already cross-platform, has Win SEH
and POSIX SIGSEGV handlers wired). Mark the 4K page containing 0xBCE251C0
as read-only at host_ns = 9.4 s (just before the install epoch); the page
fault triggers the writer's host instruction, log RIP/host stack, then
unprotect+resume.
Pros: catches the writer with bytecode-level precision regardless of how it writes (memcpy, raw assign, vector store, etc.).
Cons: ~150–200 LOC platform-gated; needs accurate epoch timing (can't trap the whole boot or it crashes). Use host_ns ≥ 9.0 s as the gate.
Path 3 — Kernel-handler grep with new ε-constraint
Now that the install epoch is known (9.4–9.6 s host_ns; just AFTER
DiscImageDevice::ResolvePath(\\dat\\movie) per AUDIT-058 narrative), grep
all kernel handlers for ones that fire in that window AND write to the
heap. The probe log already shows this is right around the time
HostPathDevice::ResolvePath(\\dat\\movie) runs and various worker file IO
starts. Cross-reference with canary's existing kernel-call trace
(--log_level=4) to enumerate handlers called in the 9.0–9.7 s window.
LOC: 0 (purely investigative).
Recommended Session 4 priority: Path 1 first (concrete instrumentation extends what we have, leverages the epoch constraint). Path 2 as backstop. Path 3 alongside as a cheap parallel investigation.
Cascade outcome (Session 3)
- A: identify install epoch — PASS (9.4–9.6 s host_ns; ~966 ms before sub_825070F0).
- B: identify neighbor pattern — PASS (3-slot simultaneous write, POD struct signature confirmed).
- C: confirm reading-error #36 — PASS (Run 10 demonstrates host-write
surfaces miss the install even with the CORRECT target value
0x8200A1E8). - D: identify the host-side writer — N/A (Session 4 work, with epoch and signature constraints to narrow the search).
- E: secondary discovery: actual vptr is
0x8200A1E8not0x8200A208— PASS (AUDIT-067's target value was off by 32 bytes; may have contributed to that audit's 0-hit JIT store result).
Net 4/5 wins. Session 4 has concrete constraints (epoch, signature, value correction) to land the writer identification.
Reading-error class #36 reinforcement
Session 3 directly demonstrates reading-error #36 (POD-struct copy-assignment bypass for typed BE/LE field watch). The corrective rule is now formalized as:
When hooking host-side writes to guest memory, member-level set() hooks (e.g.
xe::endian_store::set()) catch ONLY explicit assignments like*be<T>* = value. They DO NOT catch:
- POD struct copy-assignment (
*reinterpret_cast<X*>(host_ptr) = X{...}).- memcpy into the host pointer (
memcpy(host_ptr_from_TranslateVirtual, &local_struct, sizeof(X))).- Vector-typed bulk store intrinsics that target guest memory.
Mitigation: pair host-write hooks with read-mode probes at the target VA — the read probe captures the install regardless of the writer's mechanism, and provides epoch + neighbor-pattern constraints for the follow-up targeted instrumentation.
This rule is now reflected in the AUDIT-068 Session 3 read-probe machinery — preserved in canary tree for all future audits.
Discipline observed
--mute=trueon every run ✓- Cold-protocol: cache wipe before each cold run; cache restored from
/tmp/canary-cache-bak-audit-068at session end ✓ (current cache was backed up at session start since prior backup was missing). - xenia-rs HEAD
e6d43a23…UNCHANGED ✓ (verified by sha256 ofgit diff HEADat session start vs end; uncommitted modifications from prior sessions are unchanged from session start, no new modifications made by this session). - Canary instrumentation purely additive + cvar-gated default-off ✓
- No destructive shortcuts ✓
- Static-init gate pattern preserved + extended (Session 3's read probe
thread is also gated on
g_guest_to_host_thunk + g_query_protect_thunkbeing non-null — same discipline as Session 2's thunk gate).
Artifacts (this dir)
fix-canary-v3.diff— cumulative Session 3 instrumentation (this run).run6-read-probe-bisect.log— primary probe on0xBCE25340(90 s; 7 changes, ended at0x820610E0, never0x8200A208).run7-read-probe-neighbors.log— bisect probe on0xBCE25340 ± 4/8; 3 simultaneous writes at+0/+4/+8confirming POD signature.run9-read-probe-251C0-neighbors.log— neighbor probe on the actual ctx_ptr0xBCE251C0; captures the install at host_ns=9.416 s.run10-cross-validation.log— read probe + host-write watch with CORRECT value0x8200A1E8; demonstrates 0 HOST-WRITE hits while read probe sees the install at host_ns=9.612 s.writer-report-v3.md— this file.
(Run 8 was an intermediate diagnostic; data is included in Run 9/10 logs.)
Phase B / progression
image_loaded_sha256 ea8d160e…UNCHANGED (instrumentation does not touch XEX image processing).- xenia-rs HEAD UNCHANGED.
- No progression-metric movement (Session 3 is instrumentation-only). Session 4 has concrete leads.