From bcd018659bb1bdd7d33f91c00cb47ce1b7dcb2ac Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 12:13:42 +0200 Subject: [PATCH] [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) --- crates/xenia-app/src/main.rs | 43 ++++++++++++++++++++++++++++++++ crates/xenia-kernel/src/state.rs | 28 +++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index cc03d82..0bc4922 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -259,6 +259,18 @@ enum Commands { /// `--audit-pc-probe-hex=82172D88,82172D80`. #[arg(long)] audit_pc_probe_hex: Option, + /// AUDIT-2BF round 14 — guest VA (hex, optional `0x` prefix) to + /// dereference 3 deep on every `--audit-pc-probe-hex` fire. + /// Emits a paired `AUDIT-MEM-READ` line with the singleton value, + /// vtable, vtable[0] (= first virtual method, the bctrl target + /// at `0x822F1B4C`), and vtable[24] (= slot 6 = canary's silph + /// chain target `sub_821B55D8`). Compare ours vs canary to + /// determine whether the bctrl dispatches to the same function + /// or a different one. Read-only; lockstep digest unaffected. + /// Settable via `XENIA_AUDIT_MEM_READ`. Example: + /// `--audit-mem-read-hex=828E1F08`. + #[arg(long)] + audit_mem_read_hex: Option, }, /// Browse XISO disc image contents Browse { @@ -423,6 +435,7 @@ fn main() -> Result<()> { lr_trace, lr_trace_out, audit_pc_probe_hex, + audit_mem_read_hex, } => cmd_exec( &path, max_instructions, @@ -450,6 +463,7 @@ fn main() -> Result<()> { lr_trace.as_deref(), lr_trace_out.as_deref(), audit_pc_probe_hex.as_deref(), + audit_mem_read_hex.as_deref(), ), Commands::Browse { path } => cmd_browse(&path), Commands::Info { path } => cmd_info(&path), @@ -682,6 +696,7 @@ fn cmd_exec( lr_trace: Option<&str>, lr_trace_out: Option<&str>, audit_pc_probe_hex: Option<&str>, + audit_mem_read_hex: Option<&str>, ) -> Result<()> { cmd_exec_inner( path, @@ -710,6 +725,7 @@ fn cmd_exec( lr_trace, lr_trace_out, audit_pc_probe_hex, + audit_mem_read_hex, None, None, false, @@ -757,6 +773,7 @@ fn cmd_check( None, // lr_trace — same None, // lr_trace_out — same None, // audit_pc_probe_hex — diagnostic, never wanted on goldens + None, // audit_mem_read_hex — same out, expect, stable_digest, @@ -790,6 +807,7 @@ fn cmd_exec_inner( lr_trace: Option<&str>, lr_trace_out: Option<&str>, audit_pc_probe_hex: Option<&str>, + audit_mem_read_hex: Option<&str>, digest_out: Option<&str>, digest_expect: Option<&str>, stable_digest: bool, @@ -1220,6 +1238,31 @@ fn cmd_exec_inner( } } + // AUDIT-2BF round 14 — `--audit-mem-read-hex=828E1F08`. Single + // hex VA (optional `0x` prefix). Stored on `kernel.audit_mem_read_addr`. + // Paired with `audit_pc_probe_pcs`: on every probe fire, the kernel + // emits a second `AUDIT-MEM-READ` line dereferencing 3 deep so we can + // resolve vtable[0] / vtable[24] at the singleton. + let audit_mem_read_combined: Option = match ( + audit_mem_read_hex, std::env::var("XENIA_AUDIT_MEM_READ").ok(), + ) { + (Some(s), _) => Some(s.to_string()), + (None, Some(s)) if !s.is_empty() => Some(s), + _ => None, + }; + if let Some(tok) = audit_mem_read_combined { + let tok = tok.trim(); + if !tok.is_empty() { + let hex = tok.strip_prefix("0x").or_else(|| tok.strip_prefix("0X")).unwrap_or(tok); + let addr = u32::from_str_radix(hex, 16) + .map_err(|e| anyhow::anyhow!("--audit-mem-read-hex {tok:?}: {e}"))?; + kernel.audit_mem_read_addr = Some(addr); + if !quiet { + tracing::info!("audit-mem-read armed: {:#010x}", addr); + } + } + } + // Diagnostic. Parse `--dump-addr=0x828F3D08,...` (or // `XENIA_DUMP_ADDR=...`) into `kernel.dump_addrs`. The contents // are dumped at end-of-run by `dump_thread_diagnostic`. Pure diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 9879add..b0cea13 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -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, + /// 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, /// 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