[Audit] --audit-mem-dump-chain: deref a guest address N levels for diagnosis

Round-14 of AUDIT-2BF (singleton-dump). The bctrl at sub_822F1AA8+0x90
(PC 0x822F1B4C) loads [0x828E1F08] (a global singleton), dereferences
its vtable, and indirect-calls vtable[0]. Canary returns; ours hangs.
To name the resolved target we need to dump the (singleton, vtable,
vtable[0]) chain on probe firing.

Adds `--audit-mem-read-hex` / `XENIA_AUDIT_MEM_READ` taking a single
guest VA. When set and any `--audit-pc-probe-hex` PC fires, the kernel
emits a paired `AUDIT-MEM-READ` line with three guest reads:

  AUDIT-MEM-READ addr=0x828E1F08 val=<*addr> vtable=<**addr> \
                 vtable[0]=<***addr+0> vtable[24]=<***addr+24> ...

`vtable[24]` is included as the slot-6 method (audit-059 round 9
documented the canary silph chain dispatching slot 6 of a vtable here).

Read-only; lockstep digest unaffected. ~30 LOC across state.rs and
main.rs. `cmd_check` opts out of the flag (same policy as the existing
audit_pc_probe_hex).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 12:13:42 +02:00
parent 09e59e09b7
commit bcd018659b
2 changed files with 71 additions and 0 deletions

View File

@@ -260,6 +260,16 @@ pub struct KernelState {
/// digest unaffected. Settable via `--audit-pc-probe-hex` /
/// `XENIA_AUDIT_PC_PROBE`.
pub audit_pc_probe_pcs: std::collections::HashSet<u32>,
/// AUDIT-2BF round 14 — diagnostic. Optional guest VA. When set, each
/// `AUDIT-PC-PROBE` fire emits a paired `AUDIT-MEM-READ` line with
/// `addr`, `*addr` (singleton value), `**addr` (vtable), `***addr+0`
/// (vtable[0] = first virtual method), and `***addr+24` (vtable[6]
/// in 4-byte stride = slot 6 = silph chain bctrl target). Three-deep
/// dereference to resolve the vtable[0] target at the bctrl site
/// `0x822F1B4C` inside `sub_822F1AA8`. Read-only; lockstep digest
/// unaffected. Settable via `--audit-mem-read-hex` /
/// `XENIA_AUDIT_MEM_READ`.
pub audit_mem_read_addr: Option<u32>,
/// M12 — diagnostic. PCs at which to emit a structured JSONL record
/// per fire, designed for diffing against xenia-canary's
/// `--log_lr_on_pc` patch output. Each line carries
@@ -344,6 +354,7 @@ impl KernelState {
pc_probe_consumers: HashMap::new(),
branch_probe_pcs: std::collections::HashSet::new(),
audit_pc_probe_pcs: std::collections::HashSet::new(),
audit_mem_read_addr: None,
lr_trace_pcs: std::collections::HashSet::new(),
lr_trace_writer: None,
dump_addrs: Vec::new(),
@@ -858,6 +869,23 @@ impl KernelState {
pc, tid, hw_id, cycle, lr, r3, r11,
vtable, slot6_method, aux_handle, sub_vt,
);
// AUDIT-2BF round 14 — paired memory-read. When
// `audit_mem_read_addr` is set, dereference 3 deep: singleton
// pointer → vtable → vtable[0] / vtable[24]. Defensively
// null-checks each level. `read_u32` returns 0 for unmapped
// pages so all-zero output is the unmapped/uninitialized
// signature.
if let Some(addr) = self.audit_mem_read_addr {
let val = mem.read_u32(addr);
let vt = if val != 0 { mem.read_u32(val) } else { 0 };
let m0 = if vt != 0 { mem.read_u32(vt) } else { 0 };
let m6 = if vt != 0 { mem.read_u32(vt.wrapping_add(24)) } else { 0 };
println!(
"AUDIT-MEM-READ addr={:#010x} val={:#010x} vtable={:#010x} \
vtable[0]={:#010x} vtable[24]={:#010x} pc={:#010x} tid={} cycle={}",
addr, val, vt, m0, m6, pc, tid, cycle,
);
}
}
/// M12 — diagnostic. If the live PC for HW slot `hw_id` is in