Audit-2BF.delta: add --audit-pc-probe-hex for silph-init bctrl probe
Adds a per-PC probe analogous to --lr-trace / --branch-probe but tuned
for the silph init chain's virtual-dispatch site at sub_82172BA0+0x1E8
(PC 0x82172D88, the bctrl after a 3-deep `lwz` chain that loads vtable
slot 6). Each fire emits one AUDIT-PC-PROBE line with (pc, tid, hw,
cycle, lr, r3, r11) plus four guest-memory dereferences off r3 — the
vtable, slot-6 method pointer, auxiliary handle field, and embedded
sub-object vtable — so the line can be compared head-to-head with
canary's round-9 capture (r3=0xBCCC52C0, [r3+0]=0x820A3644,
slot6=sub_821B55D8, [r3+0xC]=0xF80000D8, [r3+0x30]=0x820A1870) to
identify whether ours dispatches to the wrong vtable on a correct
object (case A) or to a wrong object entirely (case B).
Why this addition rather than reuse of an existing probe: --lr-trace
emits JSONL designed for canary-side diffing and only captures
r3/r4/r5/r6/lr (no memory dereferences); --branch-probe captures CR
flags and lr but again no memory; --ctor-probe is single-shot per PC
and walks the stack back-chain. None of them load the four indirect
fields needed to identify a vtable-shape divergence.
Implementation:
- state.rs: new HashSet<u32> field `audit_pc_probe_pcs` and helper
`fire_audit_pc_probe_if_match(hw_id, mem)`. Empty-set fast-path
keeps the cost to one is_empty() check per worker_prologue call
when the flag is unused. Read-only — no guest state mutation,
lockstep digest unchanged.
- main.rs: new CLI flag --audit-pc-probe-hex with bare-hex comma
parsing (tolerates `0x` prefix), settable also via
XENIA_AUDIT_PC_PROBE env var. Threaded through cmd_exec_inner;
cmd_check passes None so check digests are unaffected.
Probe wired into worker_prologue alongside fire_ctor_probe / fire_-
branch_probe / fire_lr_trace. Like its siblings, it fires once per
basic-block entry — known limitation (audit-045 reading-error class
13); use a block-entry PC if probing a mid-block instruction.
Verification: kernel 127/127, app 5/5 non-ignored, no behaviour
change with empty flag.
Cross-references audit-059 round 9's canary capture and lays the
groundwork for the round-10 ours-side comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,23 @@ enum Commands {
|
||||
/// line). Stdout when omitted.
|
||||
#[arg(long)]
|
||||
lr_trace_out: Option<String>,
|
||||
/// AUDIT-2BF — comma-separated list of guest PCs (hex, no `0x`
|
||||
/// prefix required) to capture as one-line `AUDIT-PC-PROBE`
|
||||
/// records on every fire. Designed for the silph init chain
|
||||
/// virtual-dispatch site at `sub_82172BA0+0x1E8` (PC
|
||||
/// `0x82172D88`, a `bctrl` after a 3-deep vtable-slot-6 load).
|
||||
/// Each record carries (pc, tid, hw, cycle, lr, r3, r11) plus
|
||||
/// four guest-memory dereferences off r3: `[r3+0]` (vtable),
|
||||
/// `[[r3+0]+24]` (slot 6 method = bctrl target), `[r3+0x0C]`
|
||||
/// (auxiliary handle), `[r3+0x30]` (embedded sub-object vtable).
|
||||
/// Compares directly against canary's round-9 capture:
|
||||
/// r3=0xBCCC52C0, [r3+0]=0x820A3644, slot6=sub_821B55D8,
|
||||
/// [r3+0xC]=0xF80000D8, [r3+0x30]=0x820A1870. Read-only;
|
||||
/// lockstep digest unaffected. Settable via
|
||||
/// `XENIA_AUDIT_PC_PROBE`. Example:
|
||||
/// `--audit-pc-probe-hex=82172D88,82172D80`.
|
||||
#[arg(long)]
|
||||
audit_pc_probe_hex: Option<String>,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
@@ -405,6 +422,7 @@ fn main() -> Result<()> {
|
||||
probe_db,
|
||||
lr_trace,
|
||||
lr_trace_out,
|
||||
audit_pc_probe_hex,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
@@ -431,6 +449,7 @@ fn main() -> Result<()> {
|
||||
probe_db.as_deref(),
|
||||
lr_trace.as_deref(),
|
||||
lr_trace_out.as_deref(),
|
||||
audit_pc_probe_hex.as_deref(),
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
@@ -662,6 +681,7 @@ fn cmd_exec(
|
||||
probe_db: Option<&str>,
|
||||
lr_trace: Option<&str>,
|
||||
lr_trace_out: Option<&str>,
|
||||
audit_pc_probe_hex: Option<&str>,
|
||||
) -> Result<()> {
|
||||
cmd_exec_inner(
|
||||
path,
|
||||
@@ -689,6 +709,7 @@ fn cmd_exec(
|
||||
probe_db,
|
||||
lr_trace,
|
||||
lr_trace_out,
|
||||
audit_pc_probe_hex,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
@@ -735,6 +756,7 @@ fn cmd_check(
|
||||
None, // probe_db — same
|
||||
None, // lr_trace — same
|
||||
None, // lr_trace_out — same
|
||||
None, // audit_pc_probe_hex — diagnostic, never wanted on goldens
|
||||
out,
|
||||
expect,
|
||||
stable_digest,
|
||||
@@ -767,6 +789,7 @@ fn cmd_exec_inner(
|
||||
probe_db: Option<&str>,
|
||||
lr_trace: Option<&str>,
|
||||
lr_trace_out: Option<&str>,
|
||||
audit_pc_probe_hex: Option<&str>,
|
||||
digest_out: Option<&str>,
|
||||
digest_expect: Option<&str>,
|
||||
stable_digest: bool,
|
||||
@@ -1167,6 +1190,36 @@ fn cmd_exec_inner(
|
||||
}
|
||||
}
|
||||
|
||||
// AUDIT-2BF — `--audit-pc-probe-hex=82172D88,...`. Bare-hex tokens
|
||||
// (with or without `0x` prefix). Parses every comma-separated entry
|
||||
// as a u32 PC and inserts into `kernel.audit_pc_probe_pcs`. Empty
|
||||
// set is the hot-path no-op (single is_empty() check).
|
||||
let audit_pc_probe_combined: Option<String> = match (
|
||||
audit_pc_probe_hex, std::env::var("XENIA_AUDIT_PC_PROBE").ok(),
|
||||
) {
|
||||
(Some(s), _) => Some(s.to_string()),
|
||||
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(list) = audit_pc_probe_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let hex = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")).unwrap_or(token);
|
||||
let pc = u32::from_str_radix(hex, 16)
|
||||
.map_err(|e| anyhow::anyhow!("--audit-pc-probe-hex {token:?}: {e}"))?;
|
||||
kernel.audit_pc_probe_pcs.insert(pc);
|
||||
}
|
||||
if !quiet && !kernel.audit_pc_probe_pcs.is_empty() {
|
||||
let mut pcs: Vec<u32> = kernel.audit_pc_probe_pcs.iter().copied().collect();
|
||||
pcs.sort_unstable();
|
||||
let strs: Vec<String> = pcs.iter().map(|p| format!("{p:#010x}")).collect();
|
||||
tracing::info!(
|
||||
"audit-pc-probe armed: {} ({})",
|
||||
kernel.audit_pc_probe_pcs.len(),
|
||||
strs.join(", "),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2242,6 +2295,7 @@ fn worker_prologue(
|
||||
// the helper, no overhead on the hot path.
|
||||
kernel.fire_ctor_probe_if_match(hw_id, mem);
|
||||
kernel.fire_branch_probe_if_match(hw_id);
|
||||
kernel.fire_audit_pc_probe_if_match(hw_id, mem);
|
||||
kernel.fire_lr_trace_if_match(hw_id);
|
||||
|
||||
if mem.has_mem_watch() {
|
||||
|
||||
@@ -244,6 +244,22 @@ pub struct KernelState {
|
||||
/// Distinct from `ctor_probe_pcs` because that helper emits 8
|
||||
/// frames of back-chain per hit — too noisy for branch tracing.
|
||||
pub branch_probe_pcs: std::collections::HashSet<u32>,
|
||||
/// AUDIT-2BF — diagnostic. PCs at which to emit a structured one-line
|
||||
/// `AUDIT-PC-PROBE` record on every fire, designed for the silph init
|
||||
/// chain virtual-dispatch site at `sub_82172BA0+0x1E8` (PC
|
||||
/// `0x82172D88`, a `bctrl` after a 3-deep load of vtable slot 6). The
|
||||
/// emitted line carries (pc, tid, hw, cycle, lr, r3, r11) plus four
|
||||
/// guest-memory dereferences off `r3`: `[r3+0]` (vtable), `[[r3+0]+24]`
|
||||
/// (slot 6 method pointer = the bctrl target), `[r3+0x0C]` (audit-059
|
||||
/// round-9 canary-known auxiliary handle `0xF80000D8`), and `[r3+0x30]`
|
||||
/// (canary-known embedded sub-object vtable `0x820A1870`). Distinct
|
||||
/// from `branch_probe_pcs` because that helper only logs registers (no
|
||||
/// memory) and from `lr_trace_pcs` because that emits JSON intended
|
||||
/// for canary diffing, not the four hard-coded indirect dereferences
|
||||
/// needed here. Read-only — no guest state mutation. Lockstep
|
||||
/// digest unaffected. Settable via `--audit-pc-probe-hex` /
|
||||
/// `XENIA_AUDIT_PC_PROBE`.
|
||||
pub audit_pc_probe_pcs: std::collections::HashSet<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
|
||||
@@ -327,6 +343,7 @@ impl KernelState {
|
||||
ctor_probe_pcs: std::collections::HashSet::new(),
|
||||
pc_probe_consumers: HashMap::new(),
|
||||
branch_probe_pcs: std::collections::HashSet::new(),
|
||||
audit_pc_probe_pcs: std::collections::HashSet::new(),
|
||||
lr_trace_pcs: std::collections::HashSet::new(),
|
||||
lr_trace_writer: None,
|
||||
dump_addrs: Vec::new(),
|
||||
@@ -797,6 +814,52 @@ impl KernelState {
|
||||
);
|
||||
}
|
||||
|
||||
/// AUDIT-2BF — diagnostic. If the live PC for HW slot `hw_id` is in
|
||||
/// `self.audit_pc_probe_pcs`, emit a single one-line
|
||||
/// `AUDIT-PC-PROBE` record with (pc, tid, hw, cycle, lr, r3, r11)
|
||||
/// plus four guest-memory dereferences off r3: `[r3+0]` (vtable),
|
||||
/// `[[r3+0]+24]` (slot 6 method = bctrl target), `[r3+0x0C]`
|
||||
/// (auxiliary handle field), `[r3+0x30]` (embedded sub-object
|
||||
/// vtable field). Tuned for the silph init chain virtual-dispatch
|
||||
/// site at `sub_82172BA0+0x1E8` (PC `0x82172D88`).
|
||||
///
|
||||
/// Read-only. No guest-state mutation; lockstep digest unaffected.
|
||||
/// Empty set is the common case → single `is_empty()` test on the
|
||||
/// hot path.
|
||||
pub fn fire_audit_pc_probe_if_match(&self, hw_id: u8, mem: &GuestMemory) {
|
||||
if self.audit_pc_probe_pcs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.scheduler.ctx(hw_id);
|
||||
let pc = ctx.pc;
|
||||
if !self.audit_pc_probe_pcs.contains(&pc) {
|
||||
return;
|
||||
}
|
||||
let tid = self.scheduler.tid(hw_id).unwrap_or(0);
|
||||
let r3 = ctx.gpr[3] as u32;
|
||||
let r11 = ctx.gpr[11] as u32;
|
||||
let lr = ctx.lr as u32;
|
||||
let cycle = ctx.cycle_count;
|
||||
// Memory dereferences. Guest pointers may be unmapped/garbage;
|
||||
// `read_u32` returns 0 for unmapped pages (heap.rs:510 returns
|
||||
// a default), so an all-zero block in the output reliably
|
||||
// indicates an invalid `r3`.
|
||||
let vtable = mem.read_u32(r3);
|
||||
let slot6_method = if vtable != 0 {
|
||||
mem.read_u32(vtable.wrapping_add(24))
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let aux_handle = mem.read_u32(r3.wrapping_add(0x0C));
|
||||
let sub_vt = mem.read_u32(r3.wrapping_add(0x30));
|
||||
println!(
|
||||
"AUDIT-PC-PROBE pc={:#010x} tid={} hw={} cycle={} lr={:#010x} r3={:#010x} r11={:#010x} \
|
||||
[r3+0]={:#010x} [[r3+0]+24]={:#010x} [r3+0x0C]={:#010x} [r3+0x30]={:#010x}",
|
||||
pc, tid, hw_id, cycle, lr, r3, r11,
|
||||
vtable, slot6_method, aux_handle, sub_vt,
|
||||
);
|
||||
}
|
||||
|
||||
/// M12 — diagnostic. If the live PC for HW slot `hw_id` is in
|
||||
/// `self.lr_trace_pcs`, emit one JSONL record. Format mirrors what
|
||||
/// xenia-canary's `--log_lr_on_pc` patch emits, plus the cycle
|
||||
|
||||
Reference in New Issue
Block a user