AUDIT-060 PROBE-O — fnptr-array bootstrap Run config - binary : xenia-rs/target/release/xenia-rs-probe (xenia-rs HEAD e6d43a2) - instr : 500_000_000 - iso : Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso - db : xenia-rs/sylpheed.db Phase 1 — CTOR-PROBE fire counts (500M instr, --quiet) PC | function | fires -----------------|-----------------------------|------ 0x824ACB38 | sub_824ACB38 (CRT driver) | 1 0x82457EF0 | sub_82457EF0 (canary "only-caller", AUDIT-059 said unreachable) | 1 0x82458B90 | sub_82458B90 (canary signaler A) | 1 0x8245EC10 | sub_8245EC10 (canary signaler B) | 2 0x8245FEB8 | sub_8245FEB8 (vptr installer, AUDIT-059 said "dead in ours") | 5 0x821B6DF4 | sub_821B6DF4 (ladder top, AUDIT-058) | 0 0x821B55D8 | sub_821B55D8 | 0 0x824F8398 | sub_824F8398 | 0 0x824F7CD0 | sub_824F7CD0 | 0 0x824F7800 | sub_824F7800 | 0 0x825070F0 | sub_825070F0 | 0 Phase 2 — sub_824ACB38 anatomy Static body (224 B, addr 0x824ACB38..0x824ACC18): +0x00..+0x2C preamble + one optional dispatch through fn-ptr at [0x82023F08] (=0x825F1630, an LZ-runtime thunk) +0x30..+0x6C loop A: enumerate u32 slots in [0x828708C8, 0x828708D4) — 3 slots filter: non-NULL bctrl at 0x824ACBA0 +0x80..+0xB8 loop B: enumerate u32 slots in [0x82870010, 0x828708C4) — 557 slots filter: non-NULL AND != 0xFFFFFFFF bctrl at 0x824ACBEC +0xC4 epilogue, blr Phase 2/3 — Array layout (post-reloc, dumped at 1M and 500M instr; both runs identical) Region 0x82870010..0x828702E8 — populated with 0x82xxxxxx pointers (vtable methods) Region 0x828702F0..0x82870580 — **PERMANENTLY ZERO** across both 1M and 500M dumps (160 of 557 slots = 28.7% of array) Region 0x82870590..0x828708C4 — populated with 0x82xxxxxx pointers (vtable methods) Region 0x828708C8..0x828708D4 — loop-A array, populated (small CRT helpers) Static-analyzer cross-check (sylpheed.db, function_pointer_arrays): The 557-slot region is NOT a single CRT init array. It contains 9+ separate small "vtable"-classified arrays (lengths 3, 9, 12, 16, 13, 3, 3, 3, 3, 3, 3, 4, 4, 3, ...) at addresses 0x82870014, 0x82870024, 0x82870094, 0x828700C8, 0x8287016C, 0x82870214, 0x82870238, 0x82870250, 0x828702A8, 0x828702C0, 0x828702E4, 0x828705A0, 0x8287062C, 0x82870870. **NO** statically-detected arrays/refs in 0x82870300.. 0x828705A0 — confirms the gap is intentional (unused padding between two clusters of small vtables). This means **sub_824ACB38 does NOT iterate a CRT static-ctor list**. It iterates **runtime vtable registration slots** — likely a class-registration table where each non-NULL entry is invoked once at load time (TLS / static-init style). AUDIT-050's framing ("CRT driver iterates 0x82870xxx fnptr arrays") is **structurally correct** (1 fire / iteration of 557 slots) but **semantically misleading**: the slots are not "static initializers feeding RegisterToFactory" — they're class vtable entries. Phase 3 — ladder-fn references sub_821B6DF4 (ladder top) appears as a value in the binary at exactly 2 places: - 0x820C1994 in .rdata — embedded as a u32 in an MSVC EH FuncInfo/UnwindMap structure (surrounding bytes: `FFFFFFFF 821B6DF4 19930522 00000001 820C1990 ...`; 0x19930522 = MSVC FuncInfo magic, so 0x821B6DF4 is a **catch-handler dispatch target**) - 0x8211C678 in .pdata — exception-unwind metadata (not a real call ref) Disasm at 0x821B6DF4 confirms: prolog `subi r31, r12, 112; mflr r12; stwu r1, -96(r1); ...` is the canonical MSVC C++ catch-handler thunk (uses r12 as parent-frame pointer offset). Body calls one bl (0x82183B78, a label inside an EH support routine) then returns. **Verdict**: the AUDIT-058 ladder `sub_821B6DF4 ← sub_821B55D8 ← ... ← sub_825070F0` is **not a normal call chain**. `sub_821B6DF4` is dispatched **only by the C++ exception runtime**, when a specific exception type is thrown during front-end-UI initialization. AUDIT-058's "static caller ladder" was reading EH handler-array linkage as if it were a call ladder. Phase 4 — surprising contradictions vs AUDIT-058 / AUDIT-059 1. `sub_82457EF0` fires 1× on tid=6 (HW=2, cycle=0, lr=0xbcbcbcbc = thread-entry sentinel). This is the THREAD ENTRY POINT for tid=6. AUDIT-059's "only-caller sub_82457EF0 has 0 callers" was correct — it has 0 *static* callers because it's a `thread_proc` invoked by `ExCreateThread`. tid=6 spawns and runs through sub_82457EF0 → sub_82458B90 in our run. 2. `sub_8245FEB8` is **NOT dead in ours** — it fires 5× total, called via: • sub_824601A0+0x68 (PC=0x82460208) — once from tid=1 boot path at cycle 5.5M (callers go ..0x82448120 ← 0x8216EC10 ← 0x824AB8E0=entry_point: this is the **dispatch_table @ 0x820B5830 slot 1** AUDIT-059 named, fired during entry-point processing — NOT dead) • 3 more times from tid=1 during later UI inflation (frames via sub_82175FBC / sub_82178FC8 / sub_82179148 / sub_82173A4C — the audit-009 "front-end UI" cluster) • 1× on tid=13 at cycle 23788 (frames via sub_821CB1D0 ← sub_821CBAE0 ← sub_821CC454 ← sub_821C4F18 = AUDIT-058's tid=13 chain) AUDIT-059's static-analysis (vptr-installer sub_8245FEB8 "dead in ours") is **FALSIFIED at runtime**. 3. `sub_8245EC10` (canary signaler B) fires 2× in ours (callers: sub_8245FEB8). Both fires are NEW confirmations — this is on the active path. Specific actionable finding ================================= The AUDIT-058 ladder is **not an activation chain**. It is the **MSVC C++ exception unwind path** for a specific exception type. `sub_821B6DF4` is a catch-handler thunk; sub_821B55D8/824F8398/824F7CD0/ 824F7800 are the throw-side functions. They fire 0× in ours not because of any "fnptr-array gap" or "cluster unreachability", but because **no exception is currently being thrown** at this stage of our boot. Canary throws (and catches) something that ours doesn't. Recommended AUDIT-061 directions (in priority order): (a) Probe `RaiseException` / `_CxxThrowException`-equivalent (Xbox xboxkrnl) and any cxx_throw site in canary vs ours. The 058 ladder fires iff a specific exception type-id is thrown — find it. (b) AUDIT-053 noted "warm-start regression (cxx_throw=10)" — that throw-counter mismatch may BE the 058 ladder firing in warm-state canary. Cross-reference cxx_throw=10 throws with the 058 ladder. (c) The Phase 1 confirmation that sub_8245FEB8 IS LIVE in ours (5 fires) means AUDIT-059's γ-investigation can scrap the "vptr-installer dead" branch. Refocus on **why our worker dispatch table at 0x820B5830 slot 1 fires but doesn't subsequently propagate signals**. (d) Optionally: AUDIT-050's "CRT driver enumerates 557 slots, 82 non-NULL" needs re-examination — the region is a **collection of vtables**, not a CRT init array. Some of the "82 non-NULL" slots are vtable methods (e.g., destructors). The fnptr-array "half-bootstrapped" framing has been super-cited in the audit chain without verifying what's actually being enumerated. Outputs - audit-runs/audit-060-fnptr-array-bootstrap/ours-phase1.stdout (CTOR-PROBE log, 11 PCs, 500M instr) - audit-runs/audit-060-fnptr-array-bootstrap/ours-dump-500M.stdout (38-region dump, post-reloc, 500M) - audit-runs/audit-060-fnptr-array-bootstrap/array1_dump.txt (static, pre-reloc — INVALID due to reloc — kept for reference) - audit-runs/audit-060-fnptr-array-bootstrap/array2_dump.txt (static, pre-reloc — INVALID — kept for reference) Discipline gate - xenia-rs source unmodified (READ-ONLY discipline upheld). - Stop-hook safe (binary renamed to xenia-rs-probe). - No canary patch applied this round.