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

@@ -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)