[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:
@@ -259,6 +259,18 @@ enum Commands {
|
||||
/// `--audit-pc-probe-hex=82172D88,82172D80`.
|
||||
#[arg(long)]
|
||||
audit_pc_probe_hex: Option<String>,
|
||||
/// 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<String>,
|
||||
},
|
||||
/// 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<String> = 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user