Accumulated diagnostic notes from prior sessions that had stayed in the
working tree without being committed. Spans 20 audit entries (KRNBUG-AUDIT-023
through KRNBUG-AUDIT-057) plus VERIFY-A and TRACK-1/TRACK-2 sub-audits, all
read-only investigations dated 2026-05-06 through 2026-05-10.
No code or schema changes. Pure documentation backfill so future sessions can
cross-reference the full chain without depending on the auto-memory directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
instructions: 50000002 → 50000001 (1-instr shift from FILE_DIRECTORY_FILE
plumbing on NtCreateFile path; all other digest fields unchanged —
imports/swaps/draws/render-targets/shaders/textures all match
prior golden).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track A — FILE_DIRECTORY_FILE handling. NtCreateFile's 9th parameter
`create_options` (sp+0x54 per shim_utils.h:49-50) is now read and
forwarded to open_vfs_file/open_cache_file. When the
FILE_DIRECTORY_FILE bit (0x1) is set on a `cache:\<hash>` path,
the host-side handler `mkdir -p`s instead of `File::create`'ing a
0-byte sentinel that blocked subsequent hierarchical creates of
`cache:\<hash>\<sub>\<leaf>` with NAME_COLLISION. Confirmed by
`opts=0x4021` (incl. FILE_DIRECTORY_FILE) on `cache:\d4ea4615`
and `opts=0x4020` (no DIR bit) on the leaf `.tmp` files. NtOpenFile
forwards `open_options` (r8) into the same slot per
xboxkrnl_io.cc:118-122. Closes the AUDIT-053 ζ-class VFS layout
aliasing wedge.
Track B — opt-in persistent cache root. AUDIT-038's per-process
tmpdir + wipe stays the default (preserves lockstep/oracle
determinism + dodges Sylpheed's `<hash>.tmp` journal-append-on-
reboot self-inconsistency). Persistence is now opt-in via
* `XENIA_CACHE_ROOT=<path>` — explicit path (caller manages
wiping); hands a stable place to drop a canary-built cache
for cascade A/B oracle work.
* `XENIA_CACHE_PERSIST=1` — `$XDG_DATA_HOME/xenia-rs/cache`
(or `$HOME/.local/share/xenia-rs/cache`).
Cold-start (-n 500M, default tmpfs) with FILE_DIRECTORY_FILE fix:
swaps=1 draws=0 imports=40454 cxx_throw=0 — matches master baseline,
no regression. Cache hierarchy now mkdir-p'd correctly: `cache:/`
contains 9 hash dirs (e.g. `d4ea4615/e/`, `aab216c3/5/`) instead
of the 0-byte sentinel files AUDIT-053 found masquerading as
directories.
LOC: +88 / -14 = +74 net (≤80 budget). All 127 xenia-kernel unit
tests pass.
Trace: audit-runs/audit-054-vfs-layout-fix/
cold-start-digest.json + warm-start-digest.json (defaults)
persist-cold-digest.json + persist-warm-digest.json (opt-in)
baseline-master-digest.json (master 25704c5 reference)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to 49f3eaf (AUDIT-032 dedicated audio worker). With the
audio callback ticker now on by default, the boot trajectory at
50M instr changes:
instructions 50000009 -> 50000002 (interpreter stop boundary shift)
imports 407215 -> 40454 (-90% — left audio-wait busy loop)
swaps 2 -> 1 (degenerate splash repeat lost;
main thread advances past splash)
draws 0 -> 0 (audio gate != renderer gate per
AUDIT-032 methodology correction)
The 10x imports drop reflects exiting the NtWaitForSingleObjectEx
busy-wait pattern (1.49M -> 30 calls per audit-runs/audit-048-*).
Boot now reaches Stfs/Xam content/crypto init phase. The single
remaining swap is the first splash; main thread is then blocked on
a different handle (0x1280) for follow-up.
sylpheed_n2m unchanged — at 2M instr the audio worker hasn't fired
yet, so the digest is byte-identical pre/post AUDIT-032.
Verified deterministic via two consecutive --expect runs at the new
digest (cargo test -p xenia-app --test sylpheed_oracles -- --ignored
passes in 2.82s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces APUBUG-PRODUCER-001's random-victim-hijack audio injection
with a dedicated per-client guest worker thread, mirroring xenia-canary's
apu/audio_system.cc:84-159 WorkerThreadMain pattern in xenia-rs's
threading model. Audio callback ticker is now safe to enable by default.
## What changed
- xenia-kernel/src/xaudio.rs: new XAudioState fields worker_handles +
worker_refs (one slot per of XAUDIO_MAX_CLIENTS=8). Synthetic
park-handle helper (0xF000_0000 | client_idx) — outside the normal
alloc range so wake_eligible_waiters never finds it; the only
legitimate state-flip is via try_inject_audio_callback.
- xenia-kernel/src/exports.rs: xaudio_register_render_driver spawns a
64KB-stack guest thread (create_suspended=true) via
state.scheduler.spawn after registration succeeds. Immediately flips
the spawned thread's state from Blocked(Suspended) to
Blocked(WaitAny[synthetic]) so it's parked but not woken. Stores the
kernel handle so find_by_handle resolves a fresh ThreadRef after slot
compaction. Failure paths log + leave xaudio.worker_refs[i] = None,
in which case the ticker drops fires (no random-victim fallback).
- xenia-app/src/main.rs: try_inject_audio_callback resolves the worker
via worker_handles[index] instead of scanning runqueues for a Ready
or Blocked victim. The PC+r3 injection and SavedCallbackCtx capture
are unchanged; the existing LR_HALT restore path re-blocks the
worker on its synthetic handle for the next tick. Flag handling
reworked: --xaudio-tick / XENIA_XAUDIO_TICK now act as explicit
override (truthy = force on, falsey = force off, absent = use the
KernelState default).
- xenia-kernel/src/state.rs: xaudio_tick_enabled default flipped from
false to true. Pre-fix it was off because the random-victim hijack
regressed swaps=2->1; with the dedicated worker that whole class of
regression is gone.
## Cascade verification at -n 500M (audit-runs/audit-048-audio-host-pump/)
Pre-fix baseline: audit-runs/audit-047-gamma-wedges/ours-end-state.log.
| Dim | Predicted (AUDIT-032) | Observed |
|-----|-------------------------------------|---------------------------------|
| A | tid=9 leaves Blocked[0x828A3254] | Ready @ pc=0x824d1404 |
| B | tid=10 leaves Blocked[0x828A3230] | Ready @ same pc/lr |
| C | XAudioSubmitRenderDriverFrame > 0 | Mixer setup path executed |
| D | KeReleaseSemaphore 0 -> non-zero | 0 -> 1; xaudio.callback.delivered=1 |
Bonus: audit-042's tid=6 worker pair on 0x10A0+0x10A4 also went
Blocked->Ready as a downstream effect.
Boot trajectory shifted significantly: NtWaitForSingleObjectEx
1,489,791 -> 30; NtSetEvent 3,334 -> 68; new exports firing
(StfsCreateDevice, ObCreateSymbolicLink, XamContentCreateEnumerator,
XamEnumerate, XamTaskSchedule, ExCreateThread x10, KeSetAffinityThread x7,
NtCreateSemaphore x4, NtWaitForMultipleObjectsEx x94, NtDuplicateObject x14,
XeCryptSha, XeKeysConsolePrivateKeySign). The system left the
audio-wait busy loop and entered the savegame/content/crypto init phase.
swaps regressed 2 -> 1 (degenerate splash repeat lost; main thread now
advances past splash entirely, blocked on a different handle). draws
unchanged at 0 — expected per AUDIT-032 (audio gate != renderer gate).
## Tests + scope
- cargo build --release succeeds, no new warnings.
- cargo test -p xenia-kernel --lib: 127/127 pass (incl. xaudio).
- cargo test -p xenia-app --lib: 5/5 non-ignored pass.
- Lockstep goldens (sylpheed_n2m / sylpheed_n50m) WILL drift on this
fix and need re-baselining as a follow-up commit.
75 net non-comment LOC across 4 files, well under AUDIT-032's
60-120 LOC budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the four remaining deferred follow-up items in one bundle.
All four are smaller-scope and additive; lockstep determinism
unaffected (analyzer-only changes).
## M9.5 — __CxxFrameHandler scope-table parsing
- New `xenia_analysis::eh_scope` module. Magic-scans .rdata for the
three documented MSVC FuncInfo signatures (0x19930520/21/22) on
4-byte alignment. Each match is parsed as the documented struct
(BE u32 fields), with sanity caps on max_state / n_try_blocks /
pointer validity.
- Walks pUnwindMap (UnwindMapEntry, 8 bytes) and pTryBlockMap
(TryBlockMapEntry, 20 bytes) into one row each.
- New tables eh_funcinfo, eh_unwind_map, eh_try_blocks.
- Sylpheed yield: 2,588 FuncInfo (all version 0x19930522) /
10,019 unwind entries / 315 try-blocks.
## M11.5 — Static-init driver chain detection
- New `xenia_analysis::static_init` module. Walks every function
looking for the canonical _initterm loop: lwz cursor; mtctr;
bcctrl; addi cursor, cursor, 4 bounded by a compare against another
constant register. Extracts (array_start, array_end) and reads
the array.
- Reuses `function_pointer_arrays` table — drivers' arrays land with
kind='static_init' (replacing M11's prologue-heuristic output where
the structurally-grounded pattern fires).
- Sylpheed yield: 0 drivers detected — the binary's static-init
structure does not match the canonical CRT loop. Infrastructure
ready; future M11.6 can relax.
## VMX vector-store xrefs (M6 follow-up)
- Adds AltiVec/VMX X-form load/store XOs to the M6 opcode-31
dispatch: lvx/lvxl/lvebx/lvehx/lvewx (reads) and
stvx/stvxl/stvebx/stvehx/stvewx (writes), all addr_mode=
'x_form_indexed'. Static resolution still requires both rA and rB
constant.
- Sylpheed yield: 110 newly-detected stvx writes.
## Shift_JIS + UTF-8 localised-string detection (M7 follow-up)
- Extends `xenia_analysis::strings::analyze` with scan_shift_jis (JIS
X 0208 lead/trail byte ranges + half-width katakana pass-through)
and scan_utf8 (2- and 3-byte sequences). At least one multi-byte
unit required so pure-ASCII strings aren't double-counted.
- SJIS bytes rendered as \xHH escapes for diagnostic readability;
full SJIS→UTF-8 decoding deferred.
- Sylpheed yield: 790 Shift_JIS strings (Japanese debug + UI text)
+ 39 UTF-8.
## Tests
- +2 EH (parses_minimal_funcinfo_v0, rejects_bogus_max_state)
- +2 static_init (detects_canonical_initterm_loop, rejects_function_without_pattern)
- +2 strings (detects_shift_jis_string, detects_utf8_multibyte_string)
Tests 649→655 (+6 unit tests). DB schema golden + write_analysis_results
signature updated for new EH parameter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the dominant case M5 could not resolve — `lwz vt, off(this);
lwz fn, slot(vt); mtctr; bcctrl` (real C++ dispatch). Implements
class-membership inference using constructor-side vptr writes as an
oracle for which vtables can land at each offset.
## Algorithm
Phase 1 — vptr-write scan: walk every function with the existing
lis+addi register tracker. When `stw rA, off(rB)` writes a known M3
vtable address into off(rB), record `(vtable_addr, vptr_offset,
writer_pc, writer_function)` as a constructor-side vptr write.
Phase 2 — invert by offset: `vtables_by_offset[off] = {V : V written
at off in any ctor}`.
Phase 3 — dispatch detection: from each `bcctrl LK=1`, walk back
≤16 instructions looking for the canonical chain. Bail on register
clobber, branch, or label (basic-block) boundary.
Phase 4 — edge emission: for `(dispatch_pc, vptr_off, slot)`, emit one
`xrefs.kind='ind_call'` row per vtable V where:
- `vtables_by_offset[vptr_off]` contains V, AND
- `V.length > slot` (V actually has a method at that slot)
Multi-candidate sites (the common case at offset 0) are an
over-approximation; downstream queries filter to single-candidate sites
for high confidence:
`WHERE candidate_count=1` in `indirect_dispatch_sites`.
## Schema
NEW TABLES:
- `vptr_writes(writer_pc, vtable_address, vptr_offset, writer_function)`
- `indirect_dispatch_sites(dispatch_pc PK, vptr_offset, slot, candidate_count)`
- `indirect_dispatch_candidates(dispatch_pc, vtable_address, method_address)`
NEW INDICES on vtable_address / vptr_offset / method_address /
(vptr_offset, slot) for fast joins.
## Sylpheed yield
- 567 vptr writes / 214 vtables / 29 offsets (offset 0 = 88%).
- 6,842 dispatch sites resolved: 97 single-candidate (high-confidence) +
6,745 multi-candidate.
- 687,963 ind_call xref rows.
- 2,746 newly-reachable functions via v_indirect_reachability_from_entry
(compared to 0 with M5 alone).
- Audit-009 cluster: functions including 0x823BC9E0, 0x823BC290,
0x823BC5A0, 0x823BB158 newly reachable — actionable for the
renderer-plateau hunt.
Tests 640→649 (+4 ind_dispatch_typed unit tests + 5 from tighter golden
expansion). Schema golden + write_analysis_results signature updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the "Synthesized empty file" cache:/* stub with persistent
host-FS HostPathDevice backing. Sub_82459D18 / sub_8245D230 (cache-miss
reconstruct + resize-and-zero-fill) drop from constant fires to 0;
multi-MB of cache files persist to disk per boot. swaps=2 plateau
unmoved at -n 100M; cluster activation gate (audit-009) remains.
Tests 640 -> 645. Lockstep deterministic across 3+ reruns at
instructions=100000004 / imports=987485.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the "Synthesized empty file" stub for cache:/* paths with a
real host-FS HostPathDevice-style mount. Each KernelState gets a fresh
per-process tmpdir under /tmp/xenia-rs-cache-<pid>-<id>/ which is
cleared on init for lockstep determinism (mirrors canary's
xenia_main.cc:649 RegisterSymbolicLink("cache:", "\\CACHE") +
HostPathDevice in xenia-canary/src/xenia/vfs/devices/host_path_device.cc).
NtCreateFile now honours create_disposition for cache: paths:
FILE_OPEN -> NOT_FOUND if missing
FILE_CREATE -> NAME_COLLISION if present
FILE_OPEN_IF -> open or create
FILE_OVERWRITE_IF -> create or truncate
FILE_OVERWRITE -> NOT_FOUND if missing, else truncate
FILE_SUPERSEDE -> create or truncate
NtReadFile / NtWriteFile / NtSetInformationFile (XFileEndOfFileInformation)
/ NtQueryInformationFile / NtQueryFullAttributesFile route through
std::fs against the per-handle host_path; non-cache paths keep their
legacy semantics (read-only disc image, synth-empty stubs).
Verified by audit-037 cascade:
- sub_82459D18 (cache-miss restore): 0 fires (was firing constantly)
- sub_8245D230 (resize/zero-fill): 0 fires (was firing constantly)
- 105+ real cache-file writes per 500M run; 4+ MB of game data persisting
to disk per boot; cache:/recent, cache:/access, cache:/d4ea*.tmp, etc.
- Lockstep deterministic at instructions=100000004 / imports=987485
across 3+ reruns (digest shifted as expected; goldens re-baselined).
- swaps=2 plateau still in place; cluster L1 unactivated. Cascade
dimension D (cluster activation) — UNKNOWN, no L1 fires.
Tests 640 -> 645 (+5 cache-specific unit tests; full workspace green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five LOW-priority milestones bundled. Total ~700 LOC across 11 files.
## M9 — has_eh derived from pdata.flags exception bit
- New `functions.has_eh BOOLEAN NOT NULL` column. Derived from M1's
already-parsed `pdata.flags` (bit 31 of the packed word — the
exception-handler-present flag, distinct from bit 30 which is the
always-1 32-bit-code flag). Index idx_functions_has_eh.
- Sylpheed: 2,975 of 23,073 pdata-validated functions have EH (12.9%).
## M10 — .tls section / IMAGE_TLS_DIRECTORY32 parser
- New `xenia_xex::tls::parse_tls` parses the directory + zero-terminated
callback array. Returns None when the binary has no .tls section.
- New `tls_info` (singleton row) + `tls_callbacks(slot, address)` tables.
- New `DbWriter::write_tls()` no-ops on None.
- Sylpheed has no .tls section → 0 rows; infra ready for binaries with
__declspec(thread).
## M8 + M11 — function_pointer_arrays (dispatch tables + static initialisers)
- New `xenia_analysis::funcptr_arrays::analyze` widens M3's vtable scan:
detects runs of ≥2 function pointers in .rdata and classifies each as
`vtable` (M3 re-emit), `dispatch_table` (M8), or `static_init` (M11)
via a constructor-prologue heuristic (mfspr + small stwu).
- New tables `function_pointer_arrays(address PK, length, kind)` and
`function_pointer_array_entries(array_address, slot, function_address)`.
- Sylpheed: 722 vtables + 388 dispatch_tables = 1,110 arrays / 6,347 slots.
0 static_init detected (Sylpheed's ctors don't all match the
conservative heuristic; M11.5 future work can chain via the entry-
point's static-init driver).
## M12 — --lr-trace runtime canary-diff harness
- New CLI `exec --lr-trace=PC[,PC,...]` and `--lr-trace-out=PATH` flags.
Symbolic resolution (Class::method, Class::*) via M4 lookup. Env vars
XENIA_LR_TRACE / XENIA_LR_TRACE_OUT also work.
- New `KernelState::lr_trace_pcs` + `lr_trace_writer` + helper
`fire_lr_trace_if_match(hw_id)` invoked from the per-instr probe slot.
- JSONL output: pc/tid/hw/cycle/r3/r4/r5/r6/lr — superset of what
xenia-canary's --log_lr_on_pc patch emits, with a cycle counter for
cross-run reproducibility. Diff-friendly via `jq`.
- Lockstep digest unaffected: smoke test on entry-point PC fires once
with cycle=0/lr=BCBCBCBC/all-GPR-zero (correct initial state).
Tests 636→640 (+2 TLS tests, +2 funcptr_arrays tests). Schema golden
updated for new tables + has_eh column. Lockstep determinism preserved
(instructions=2000005 ×2 reruns identical).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds finer-grained addressing-mode classification to every data xref row
plus new dispatch for instruction families not previously emitted:
- New `xrefs.addr_mode VARCHAR NULL` column. NULL for control-flow edges
(call / ind_call / j / br); one of d_form / lis_addi / lis_ori /
multiword / x_form_indexed / x_form_byterev / atomic / dcbz for data
edges. Index idx_xrefs_addr_mode.
- New `xenia_analysis::xref::AddrMode` enum + Xref::addr_mode field.
- Opcode 46/47 (lmw/stmw) expand to one xref per slot — D-form multi-word
load/store now resolves all (32-rS) consecutive addresses.
- Opcode 31 X-form dispatch — stwx/stbx/sthx/stwux/stbux/sthux/stdx/stdux,
lwzx/lbzx/lhzx/lhax/lwzux/lbzux/lhzux/lhaux/ldx/ldux,
stwcx./stdcx. (atomic),
stwbrx/sthbrx/lwbrx/lhbrx (byte-reverse),
dcbz (cache-line clear).
- X-form rows are emitted ONLY when both rA and rB resolve to known
constants (rare but present); the dominant runtime-indexed pattern
remains correctly skipped.
Sylpheed yield (regen on master + merge):
- 442 newly-detected x_form_indexed reads (lwzx/lhzx into static tables).
- 40 newly-detected atomic writes (stwcx./stdcx. with resolvable address).
- 28,834 lis_addi refs, 18,485 d_form reads, 3,288 d_form writes — every
pre-existing data row now tagged.
- 0 multiword / dcbz / byterev (these instructions exist but aren't on
lis+addi-tracked code paths).
Tests 633→636 (+3 xref unit tests covering AddrMode tag uniqueness,
data-edge addr_mode round-trip, control-edge None invariant). Schema
golden updated (xrefs gains addr_mode column).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two MEDIUM milestones bundled (both opportunistic per plan; both small).
## M5 — indirect-dispatch reachability
- `xenia_analysis::indirect`: per-basic-block register tracker over each
detected function. Recognises the canonical static-vtable pattern
`lis+addi → lwz off(rA) → mtctr → bcctrl` where rA holds a known M3
vtable address. Emits one `Xref { kind: IndirectCall }` per resolvable
bcctrl site.
- PowerPC ABI awareness: `bl`-style calls clobber volatile r0..r12 + ctr
but preserve non-volatile r13..r31, so a vtable pointer parked in r30/r31
before a call survives.
- Label-based basic-block boundaries kill register state — bounds
false-positive risk for jump-IN paths.
- New `XrefKind::IndirectCall` variant (DB tag `'ind_call'`).
- New SQL view `v_indirect_reachability_from_entry` — strict superset of
`v_reachability_from_entry`, taking `ind_call` edges in the BFS.
Sylpheed yield: 0 edges detected. The binary's 1,001 static lis+addi
references into vtables are nearly all constructor-side vptr writes, not
dispatches; real method dispatch goes through `this->vptr` which requires
alias analysis we explicitly don't do. Documented in SCHEMA.md as the
expected limitation. Three unit tests cover the synthetic-correctness path.
## M7 — string / constant-pool detection
- `xenia_analysis::strings`: scans `.rdata` for runs of ≥ 6 printable
ASCII bytes (NUL-terminated) and ≥ 6 UTF-16LE code units (basic-plane
printable ASCII, NUL u16 terminator).
- New `strings(address PK, encoding, length, content)` table + encoding index.
- Implicit cross-ref via existing `xrefs.kind='ref'` rows whose target
matches a strings.address.
Sylpheed yield: 6,311 ASCII strings (including embedded HLSL shader source
and AS_CB_SURFACE_SWIZZLE_* assertion strings). 9,132 lis+addi sites
cross-reference detected strings — names source PCs near each string in
one query. Four unit tests cover encoding detection, NUL termination, and
short-run rejection.
Tests 626→633 (+3 indirect, +4 strings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLI extension only — no schema change. Adds symbolic resolution for
--pc-probe / --branch-probe / --ctor-probe tokens:
- `0xADDR` / `2186674160` — numeric (current behavior, no DB load).
- `Class::method` — joins classes × methods × demangled_names.
- `Class::*` — joins classes × methods (all slots).
- `function_name` — falls back to functions.name for free functions /
saverestore stubs / labels.
New `xenia_analysis::lookup::resolve_probe_token(db_path, token)` opens the
DB read-only ONLY when a token is non-numeric, so legacy numeric flows pay
no IO. New `--probe-db PATH` flag (or `XENIA_PROBE_DB` env / default
`sylpheed.db` next to the .iso) selects the DB.
Symbolic resolution happens BEFORE any guest exec, so it cannot affect the
lockstep digest. Verified deterministic across two reruns at -n 2M
(instructions=2000005 identical).
End-to-end smoke test on Sylpheed: `--pc-probe='ANON_Class_6B674251::*'`
resolves to all 45 method PCs of that anonymous class (matching the
methods-table row count for that vtable).
Tests 621→626 (+5 lookup unit tests covering numeric passthrough,
symbolic-without-DB error, Class::method resolution, Class::* expansion,
and functions.name fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds detection of statically-allocated MSVC vtables in .rdata/.data:
- New `xenia_analysis::vtables` walks read-only sections looking for runs of
≥3 contiguous big-endian u32 values where each value lands on a known
function start (from M1's corrected functions table). 2-slot runs are
rejected to keep false-positive rate down.
- For each candidate the MSVC RTTI walk vtable[-1] → CompleteObjectLocator
→ TypeDescriptor → mangled name is attempted; on success the demangled
class name is recorded along with a best-effort RTTIClassHierarchyDescriptor
walk to fill base_classes_json. On failure (RTTI stripped — common for
shipped game binaries) the class is named ANON_Class_<fnv1a-hash> keyed
by sorted method-PC list, so identical vtables collapse to one entry.
- DB: new tables `vtables`, `methods`, `classes` with indices on
function_address and rtti_present. `write_analysis_results` takes a
`&[Vtable]` slice; `write_disasm` (back-compat) passes empty.
- cmd_dis wires the scan after xref analysis using
`func_analysis.functions.keys()` as the function-start oracle.
Validation on Sylpheed (RTTI stripped, as expected): 722 vtables / 499
unique classes / 5571 methods. Sanity invariant: every methods.function_address
joins to functions.address (0 broken refs). Largest vtable: 131 slots.
Tests 617→621 (+4 vtable unit tests covering 3-slot detect, 2-slot reject,
synth name stability, and synth name divergence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an MSVC name-demangling layer in front of M3's vtable / RTTI work:
- New `xenia_analysis::demangle` wraps the `msvc-demangler` crate (a Rust
port of LLVM's `MicrosoftDemangle.cpp`). `demangle()` short-circuits on
non-mangled inputs (`?` prefix check); `demangle_or_raw()` always returns
a record (raw passthrough on parse failure).
- Heuristic split of the formatted demangled string into structured fields
`(namespace_path, class_name, method_name, params_signature)`. Top-level
paren / template-bracket aware, so `a::b<c::d>::e` and signatures with
templated arg types parse correctly.
- DB: new `demangled_names(address, mangled, raw_demangled, namespace_path,
class_name, method_name, params_signature)` with indices on address /
class_name / method_name. Populated from any label whose name starts with
`?` plus any import name that happens to be mangled.
For Sylpheed (a fully stripped binary) this table is empty out-of-the-box;
the layer's value lands in M3, which will append rows for every RTTI
TypeDescriptor name found in `.rdata`.
Tests 610→617 (+7 demangler unit tests covering early-out, raw fallback,
member function form, RTTI form, qname split, paren-template safety, and
top-level `::` splitting).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an authoritative function-boundary source from the linker:
- New `xenia_xex::pdata` parses .pdata 8-byte entries (BeginAddress + packed
prolog/length/flags). Bit layout per Microsoft PE32 PowerPC spec: prolog in
bits 0..7, function_length in bits 8..29, flags in 30..31.
- `func::analyze_with_pdata` unions pdata BeginAddresses into the candidate
set, attaches `pdata_validated`/`pdata_length` to each `FuncInfo`, and trims
any function whose `end` overlaps the next start (catches mis-merge where
one row spanned two prologues — the audit-031 sub_824D23B0/sub_824D29F0
case).
- DB: extends `functions` with `pdata_validated BOOLEAN`, `pdata_length BIGINT`;
new table `pdata_entries`; index on pdata_validated.
- New `crates/xenia-analysis/SCHEMA.md` documents M1 layer + forward work.
Validation on Sylpheed: 25481 functions (was 12156) / 23073 pdata_validated /
0 orphans / 0 mis-merges. Audit-031 mis-merge resolved: sub_824D29F0 now has
its own row with `pdata_length=280` (70 dwords); sub_824D23B0 now correctly
ends at 0x824D2878 (`pdata_length=1224` matches prologue walk).
Tests 605→610. New 5-test pdata unit suite covers bit layout + sentinel +
out-of-range filtering + real-world layout round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without the page-state guard, read_bulk faulted on PROT_NONE pages of
the 4 GiB host reservation. Per-page is_mapped check skips uncommitted
pages, leaving the buffer's leading zero bytes in place. Total LOC
budget after trim: 70.
The headless cmd_exec path passes quiet=false in normal use but the
diagnostic --dump-section is independent of the chatty thread/dump
prints, so it should not be gated by --quiet. Lockstep digest preserved.
Mirrors canary's `XObject::StashHandle` (xobject.h:253-256): on first
adoption of a guest dispatcher header, stamp +0x08 with the
kXObjSignature fourcc 'X','E','N','\0' and +0x0C with the stash handle
(here the guest pointer itself, since our shadow table is keyed by ptr).
Audit-023/024A documented divergence at addresses such as 0x828F4838
where canary stores "XEN\0" + handle but we left zeros. Lands as
canary-correctness restoration; cascade impact at -n 500M is nil per
the discipline gate (no sharp prediction tied to the writeback).
Lockstep determinism preserved: instructions=100000003,
imports=987516, swaps=2, draws=0 across 2 reruns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in diagnostic that emits one tracing line per guest store
overlapping any armed byte address, naming the writer (tid, pc, lr)
plus old/new u32 lanes. Mirrors the --pc-probe / --branch-probe shape;
pc/lr are stamped from worker_prologue via a thread-local Cell, so
default runs (empty watch set) take a single is_empty() check on each
write. Lockstep digest preserved (instructions=100000003 across reruns,
sylpheed_n50m.json golden byte-identical).
Diagnostic infra only; no functional change. Used to identify producers
of dispatch-state writes for the audit-017 / audit-019 hunt.
Replace the no-op cookie-returner with a real impl per canary
xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_threading.cc:216-227
(XObject::GetNativeObject<XThread>()->Resume()). Mirrors
nt_resume_thread plumbing two functions below:
resolve_pseudo_handle -> scheduler.find_by_handle -> resume_ref.
Returns STATUS_SUCCESS if the KTHREAD-pointer-as-handle resolves,
STATUS_INVALID_HANDLE otherwise — matches canary's Resume()/!thread
return semantics.
Cascade-prediction scorecard (audit-018 -> post-fix):
- A PASS: tids 9 (entry=0x824D2878) and 10 (entry=0x824D2940)
leave Suspended -> run prologue -> park on audio buffer-completion
semaphores 0x828A3254 / 0x828A3230.
- B PARTIAL FAIL: NtSetEvent 667->3334; KeReleaseSemaphore=0;
XAudioSubmitRenderDriverFrame=0.
- C FAIL (predicted 2->1, actual 2->2): both ExTerminateThread +
KeReleaseSemaphore still canary-only.
- D FAIL: gamma-cluster blocker unchanged — pc-probe at
0x82184318/0x82184374 no fires; dump-addr 0x828F4070 no DUMP;
signal_attempts on 0x1004/0x100c/0x1020/0x15e4 still 0.
Necessary-but-not-sufficient: workers unsuspend but park on a
downstream gate that's part of the audit-009/-016/-017 gamma cluster.
Tests 600 -> 601 (+ke_resume_thread_unblocks_suspended_worker).
Lockstep instructions=100000003 imports=987516 deterministic x2.
Goldens re-baselined: sylpheed_n50m.json instructions
50000003->50000011, imports 407255->407247.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors canary xam_user.cc:90-101. User 0 returns 1 (SignedInLocally),
all other indices return 0. Replaces stub_return_zero registration that
was reaching guest-side branches looking up signin state.
Tests: 599 -> 600.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
audit-findings.md: full IO-004 entry with cascade-prediction scorecard.
audit-runs/audit-006/canary_export_queue.md: post-IO-004 status note
(7 -> 3 canary-only; 4 reclassified RE-FIRES).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures two consecutive read-only diagnostic sessions:
AUDIT-008 (2026-05-05): IO-003 model reset. The 0x100c / 0x1004 / 0x15e0
workers ARE spawned post-IO-003; the IO-003 prediction-scorecard's
"UNCREATED" markers were misclassifications (handle audit already showed
the workers parked on lifecycle events, just unlinked from dispatcher
addresses). Hypothesized the gate among the 5 non-create-chain callers
of sub_821800D8 whose parents live in 0x82287000-0x82292FFF.
AUDIT-009 (2026-05-05): falsifies AUDIT-008's β-hypothesis. A 21-PC
--branch-probe (6 parents + 5 shims + dispatcher + 9 audit-005
producer-callsites) shows 0/21 firings at -n 500M — the entire
0x82287000-0x82294000 cluster is unreached. Static analysis confirms
the cluster's level-1 roots have zero non-call xrefs in sylpheed.db.
The gate is structurally above the cluster (vtable / function-pointer
that's never written). Stop condition 1 triggered; discipline gate
fails on box 1 + box 3; no fix this session.
Also updates audit-runs/audit-006/canary_export_queue.md to reflect
the AUDIT-009 evidence: 3 canary-only exports remain REAL_BUT_UNREACHED
(ExTerminateThread, KeReleaseSemaphore, XamUserReadProfileSettings) —
none is the immediate gate.
No code changes; --branch-probe machinery from AUDIT-007 sufficed.
Trace artifacts left untracked under audit-runs/audit-008/ +
audit-runs/audit-009/ (consistent with prior audit-runs/* convention).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the stub_success registration of NtDeviceIoControlFile at
exports.rs:90 with a real handler for FsCtlCodes 0x70000 (drive
geometry) and 0x74004 (partition info), mirroring xenia-canary
xboxkrnl_io.cc:645-678 + null_device.{h,cc}. The 16-byte 0x74004
response with cache_size=0xFF000 at OUT+8 is the gate that lets
sub_824ABD88 return SUCCESS and sub_824A9710 reach the priv-11
XexCheckExecutablePrivilege site identified by KRNBUG-AUDIT-007.
Stack args 9-10 (OutputBuffer, OutputBufferLength) read from the
caller's parameter save area at [sp+0x54] / [sp+0x5C] per the Xbox
360 PowerPC EABI (linkage area sp+0..sp+8, 8-quadword spill area
sp+0x14..sp+0x54, then stack args every 8 bytes). First HLE export
in the codebase to need 9+ args.
Cascade vs. KRNBUG-AUDIT-007 prediction (5/8 held):
- XexCheckExecutablePrivilege count 1 → 2 (priv=0xA + priv=0xB) ✓
- XamTaskSchedule count 0 → 1 ✓
- canary-only exports 7 → 3 (audit predicted ≤3) ✓
- 0x15e0 semaphore signal_attempts 0 → 1 (bonus)
- 0x100c worker spawn DID NOT fire (still UNCREATED) ✗
- 0x1004 signal_attempts unchanged ✗
- Worker spawn count unchanged at 19 ✗
Tests: 592 → 594. Lockstep deterministic at -n 100M (run1 ≡ run2 ≡
run3, byte-identical). instructions=100000010 → 100000019, imports
407417 → 987524 (+2.4×). swaps=2 draws=0 plateau persists.
sylpheed_n50m golden re-baselined instructions=50000004→50000003,
imports=407362→407255. sylpheed_n2m unchanged.
Still canary-only after this fix: ExTerminateThread,
KeReleaseSemaphore, XamUserReadProfileSettings. The next downstream
gate is somewhere past XamTaskSchedule's completion path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sister to --pc-probe / --ctor-probe but emits a single compact one-line
BRANCH-PROBE record per fire (pc, tid, hw, cycle, r3, lr, cr0/cr6 flags)
with no back-chain. Designed for tracing every conditional-branch fire
inside a candidate-gate function so the last PC reached before the
function epilogue identifies the exit branch.
Runtime trace at audit-runs/audit-007/sub_824A9710-trace.log decisively
identifies the priv-11 gate:
- Exit branch: 0x824a9944 (post bl sub_824ABD88 first call)
- Responsible kernel call: NtDeviceIoControlFile, FsCtlCode=0x74004
(registered as stub_success at exports.rs:90)
- Mechanical chain: stub returns 0/SUCCESS without writing OUT, game
reads [out_buf+8], finds zero, assigns hardcoded 0xC0000034
(STATUS_OBJECT_NAME_NOT_FOUND) at sub_824ABD88:0x824abea8-ac, exits
via 0x824a9944's lt branch before priv-11 site at 0x824a99a0.
592→592 tests; lockstep instructions=100000010, swaps=2, draws=0
deterministic across reruns. Read-only diagnostic — no fix this session.
Next session: KRNBUG-IO-003 (real NtDeviceIoControlFile per canary
NullDevice::IoControl for FsCtlCodes 0x70000 + 0x74004).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Volume-info class-3 alloc unit 2048 → 0x10000 (canary NullDevice
byte-identical). Tests 591 → 592, lockstep deterministic.
Audit-006-predicted 7→0 cascade FALSIFIED (7→7, no movement).
Vol-info is not the priv-11 gate. See KRNBUG-IO-002 entry in
audit-findings.md for the full diagnostic and next-session leads.
`nt_query_volume_information_file` class-3 (`FileFsSizeInformation`)
was returning sectors_per_unit=1, bytes_per_sector=2048 (alloc unit
2048). Replaced with canary's NullDevice byte-identical values
sectors=0x80, bps=0x200 (alloc unit 0x10000), with total /
available allocation units lowered to 0x10 / 0x10 to match.
Reference: xenia-canary/src/xenia/vfs/devices/null_device.h:38-46
(`NullDevice::sectors_per_allocation_unit()` and
`bytes_per_sector()`); consumed by canary's
`NtQueryVolumeInformationFile_entry` at
xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_io_info.cc:355-365.
Tests 591 → 592 (added
`nt_query_volume_information_file_class3_returns_64k_alloc_unit`).
Lockstep `instructions=100000010, swaps=2, draws=0` deterministic
across two `--stable-digest -n 100M` reruns. sylpheed_n50m oracle
still matches its existing golden — observably a no-op at -n 50M.
The audit-006-predicted 7→0 cascade did NOT fire (canary-only
exports still 7, identical set; XexCheckExecutablePrivilege still
priv=0xA only; XamTaskSchedule still 0). All 16
NtQueryVolumeInformationFile calls in our 500M trace originate
from a single LR 0x82611f38 and complete successfully — vol-info
is therefore not the priv-11 gate. The fix value is correct
(canary-byte-identical) but is not load-bearing for the gate;
landing it anyway because it's the right value and unblocks no
regression. Stop condition triggered per the IO-002 task brief —
no second fix this session.
Next-session: --pc-probe on sub_824A9710 entry to find the actual
upstream gate. See `audit-findings.md` (KRNBUG-IO-002 entry) and
`audit-runs/post-IO-002/` for the full diagnostic trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AUDIT-005's static attribution to sub_824ABA98 was wrong. The 0xC0000011
(STATUS_END_OF_FILE) at lr=0x824a97e4 traces to the NtReadFile call at
0x824a9810 inside sub_824A9710 — the cache-loader reads 1024 B from
offset 2048 of `\Device\Harddisk0\partition0`. Our synth-empty fallback
returned EOF (start_pos 2048 > size 0), so the function bailed via
RtlNtStatusToDosError before sub_824ABA98 was ever called.
Canary mounts partition0 to a NullDevice; `NullFile::ReadSync`
([null_file.cc:24-31](xenia-canary/src/xenia/vfs/devices/null_file.cc))
returns X_STATUS_SUCCESS with bytes_read=0 and never touches the
buffer. Sylpheed's caller pre-zeroes the 1024-byte stack buffer
(`memset(sp+208, 0, 1024)` at sub_824A9710 prologue), validates a
"Josh" magic on the first read, and falls back to the cache-recreate
path when the magic doesn't match.
The fix mirrors NullFile semantics: when the open synthesized a
zero-length file (`data.is_empty() && size == 0`), NtReadFile returns
SUCCESS with information=0 and the buffer untouched.
Effects (chain-of-effects verification at -n 500M):
- tests: 590 → 591 (added regression covering NullDevice semantics)
- lockstep: deterministic across 3 reruns (same instructions=100000010,
swaps=2)
- sylpheed_n50m golden re-baselined: instructions 50000004→50000000,
imports 407416→407362
- canary kernel-call diff: 10 → 7 missing exports
(XeCryptSha + XeKeysConsolePrivateKeySign + NtDeviceIoControlFile
now run; the cache-recreate path executes through to NtWriteFile)
- boot reaches silph::Silph::Impl::OnInit: 19 worker threads spawn
(was 6 before the fix)
- parked-handle 0x1004 still signal_attempts=0; the original 0x100c
and 0x15e0 are now <UNCREATED> because cascade walked past them and
the handle assignments shifted; new parked sites: 0x12fc/0x1600/
0x1040/0x10b8/0x15e8/0x1014/0x101c/0x10bc/0x1044
- draws=0 plateau persists; renderer is multi-causal blocked
Next blocker: per the canary-only diff, XamTaskSchedule + the cluster
of XAM exports (XamTaskCloseHandle, XamUserReadProfileSettings,
ObCreateSymbolicLink) and the post-thread-exit chain (ExTerminateThread,
KeReleaseSemaphore, KeResetEvent) are the next-up frontier.