feat(kernel): KRNBUG-AUDIT-007 — --branch-probe instrumentation; sub_824A9710 exit gate identified

Sister to --pc-probe / --ctor-probe but emits a single compact one-line
BRANCH-PROBE record per fire (pc, tid, hw, cycle, r3, lr, cr0/cr6 flags)
with no back-chain. Designed for tracing every conditional-branch fire
inside a candidate-gate function so the last PC reached before the
function epilogue identifies the exit branch.

Runtime trace at audit-runs/audit-007/sub_824A9710-trace.log decisively
identifies the priv-11 gate:

- Exit branch: 0x824a9944 (post bl sub_824ABD88 first call)
- Responsible kernel call: NtDeviceIoControlFile, FsCtlCode=0x74004
  (registered as stub_success at exports.rs:90)
- Mechanical chain: stub returns 0/SUCCESS without writing OUT, game
  reads [out_buf+8], finds zero, assigns hardcoded 0xC0000034
  (STATUS_OBJECT_NAME_NOT_FOUND) at sub_824ABD88:0x824abea8-ac, exits
  via 0x824a9944's lt branch before priv-11 site at 0x824a99a0.

592→592 tests; lockstep instructions=100000010, swaps=2, draws=0
deterministic across reruns. Read-only diagnostic — no fix this session.
Next session: KRNBUG-IO-003 (real NtDeviceIoControlFile per canary
NullDevice::IoControl for FsCtlCodes 0x70000 + 0x74004).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-04 21:35:10 +02:00
parent 79697ddf4e
commit c51f51f9cb
3 changed files with 229 additions and 0 deletions

View File

@@ -201,6 +201,18 @@ enum Commands {
/// runs — typically when `--halt-on-deadlock` triggers.
#[arg(long)]
dump_addr: Option<String>,
/// Diagnostic. Comma-separated list of guest PCs that emit a
/// one-line `BRANCH-PROBE` record on each fire (pc, tid, hw,
/// cycle, r3, lr, cr0/cr6 flags). Sister to `--pc-probe` but
/// without the 8-frame back-chain, suited for tracing every
/// conditional branch inside a candidate-gate function so the
/// last PC reached before the function epilogue identifies the
/// exit branch. Example:
/// `--branch-probe=0x824a9710,0x824a9778,0x824a97dc,...`.
/// Read-only; lockstep digest unaffected. Settable via
/// `XENIA_BRANCH_PROBE`.
#[arg(long)]
branch_probe: Option<String>,
},
/// Browse XISO disc image contents
Browse {
@@ -358,6 +370,7 @@ fn main() -> Result<()> {
xaudio_tick,
ctor_probe,
dump_addr,
branch_probe,
} => cmd_exec(
&path,
max_instructions,
@@ -378,6 +391,7 @@ fn main() -> Result<()> {
xaudio_tick,
ctor_probe.as_deref(),
dump_addr.as_deref(),
branch_probe.as_deref(),
),
Commands::Browse { path } => cmd_browse(&path),
Commands::Info { path } => cmd_info(&path),
@@ -581,6 +595,7 @@ fn cmd_exec(
xaudio_tick: bool,
ctor_probe: Option<&str>,
dump_addr: Option<&str>,
branch_probe: Option<&str>,
) -> Result<()> {
cmd_exec_inner(
path,
@@ -602,6 +617,7 @@ fn cmd_exec(
xaudio_tick,
ctor_probe,
dump_addr,
branch_probe,
None,
None,
false,
@@ -642,6 +658,7 @@ fn cmd_check(
xaudio_tick,
None, // ctor_probe — diagnostic, never wanted on goldens
None, // dump_addr — same
None, // branch_probe — diagnostic, never wanted on goldens
out,
expect,
stable_digest,
@@ -668,6 +685,7 @@ fn cmd_exec_inner(
xaudio_tick: bool,
ctor_probe: Option<&str>,
dump_addr: Option<&str>,
branch_probe: Option<&str>,
digest_out: Option<&str>,
digest_expect: Option<&str>,
stable_digest: bool,
@@ -972,6 +990,33 @@ fn cmd_exec_inner(
}
}
let branch_probe_combined: Option<String> = match (
branch_probe,
std::env::var("XENIA_BRANCH_PROBE").ok(),
) {
(Some(s), _) => Some(s.to_string()),
(None, Some(s)) if !s.is_empty() => Some(s),
_ => None,
};
if let Some(list) = branch_probe_combined {
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let pc = parse_hex_u32(token).map_err(|e| {
anyhow::anyhow!("invalid PC in --branch-probe: {token:?}: {e}")
})?;
kernel.branch_probe_pcs.insert(pc);
}
if !quiet && !kernel.branch_probe_pcs.is_empty() {
let mut pcs: Vec<u32> = kernel.branch_probe_pcs.iter().copied().collect();
pcs.sort_unstable();
let strs: Vec<String> = pcs.iter().map(|p| format!("{p:#010x}")).collect();
tracing::info!(
"branch probes armed: {} ({})",
kernel.branch_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
@@ -1952,6 +1997,7 @@ fn worker_prologue(
// Empty set is the common case → single `is_empty()` test inside
// 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);
// 1) Halt-sentinel check (per HW thread).
if pc == LR_HALT {

View File

@@ -215,6 +215,15 @@ pub struct KernelState {
/// extended syntax of `--pc-probe` / `--ctor-probe`. Read-only
/// load — does not mutate guest state.
pub pc_probe_consumers: HashMap<u32, (u32, u32)>,
/// Diagnostic. Comma-separated set of guest PCs that, when reached,
/// emit a single compact one-line `BRANCH-PROBE` record. The line
/// includes (pc, tid, hw, cycle, r3, lr, cr0.{lt,gt,eq}, cr6.{lt,gt,eq})
/// — designed for tracing every conditional-branch fire inside a
/// candidate-gate function (sub_824A9710 etc.) so the LAST PC
/// reached before function epilogue identifies the exit branch.
/// 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>,
/// Diagnostic. Guest addresses to dump (64 bytes each, hex + u32
/// lanes) at end-of-run. Populated from `--dump-addr=0x828F3D08,
/// 0x828F4070`. Used to inspect static dispatcher / job-queue /
@@ -277,6 +286,7 @@ impl KernelState {
parallel_active: false,
ctor_probe_pcs: std::collections::HashSet::new(),
pc_probe_consumers: HashMap::new(),
branch_probe_pcs: std::collections::HashSet::new(),
dump_addrs: Vec::new(),
};
crate::exports::register_exports(&mut state);
@@ -620,6 +630,39 @@ impl KernelState {
}
}
/// Diagnostic. If the live PC for HW slot `hw_id` is in
/// `self.branch_probe_pcs`, emit one compact `BRANCH-PROBE` line
/// with (pc, tid, hw, cycle, r3, lr, cr0.{lt,gt,eq}, cr6.{lt,gt,eq}).
/// No back-chain walk — designed for tracing every conditional
/// branch fire inside a candidate-gate function. Read-only.
/// Lockstep digest unaffected.
pub fn fire_branch_probe_if_match(&self, hw_id: u8) {
if self.branch_probe_pcs.is_empty() {
return;
}
let ctx = self.scheduler.ctx(hw_id);
let pc = ctx.pc;
if !self.branch_probe_pcs.contains(&pc) {
return;
}
let tid = self.scheduler.tid(hw_id).unwrap_or(0);
let r3 = ctx.gpr[3] as u32;
let lr = ctx.lr as u32;
let cycle = ctx.cycle_count;
let cr0 = &ctx.cr[0];
let cr6 = &ctx.cr[6];
println!(
"BRANCH-PROBE pc={:#010x} tid={} hw={} cycle={} r3={:#010x} lr={:#010x} cr0={}{}{} cr6={}{}{}",
pc, tid, hw_id, cycle, r3, lr,
if cr0.lt { 'L' } else { '.' },
if cr0.gt { 'G' } else { '.' },
if cr0.eq { 'E' } else { '.' },
if cr6.lt { 'L' } else { '.' },
if cr6.gt { 'G' } else { '.' },
if cr6.eq { 'E' } else { '.' },
);
}
/// Read a TLS slot for the currently running HW thread.
pub fn tls_get(&self, index: u32) -> u64 {
self.scheduler.tls_get(index)