feat(kernel): KRNBUG-AUDIT-005 — --pc-probe extension + canary diff identifies XexCheckExecutablePrivilege stub cascade
Extends `--ctor-probe` machinery into `--pc-probe` (clap alias) with
the optional `PC@DISPATCHER:OFFSET` token form: on a hit, the helper
additionally logs `[disp+off]` — what the producer's
`lwz r3, OFFSET(r3)` is about to read. Reuses `parse_hex_u32`; both
flags share parser + storage.
Read-only diagnostic. Lockstep digest preserved (`run digest matches
golden` at -n 50M `--stable-digest`). 588 tests green.
Decisive findings (full deliverable in `audit-findings.md` /
`audit-runs/audit-005/`):
- Failure mode α confirmed for KRNBUG-AUDIT-004: all 9 producer call
sites for handles 0x100c (5 sites) and 0x15e0 (4 sites) fire 0x at
-n 500M. The producer code path is not reached.
- Set-diff of kernel-call sequences (canary.log oracle vs ours.log
at -n 500M) identifies 11 exports canary calls and we don't:
XGetAVPack, XeCryptSha, XeKeysConsolePrivateKeySign,
ObCreateSymbolicLink, NtDeviceIoControlFile (×2),
XamUserReadProfileSettings (×2), XamTaskSchedule, XamTaskCloseHandle,
KeReleaseSemaphore (×268), KeResetEvent, ExTerminateThread (×2).
- XGetAVPack has exactly one caller (sub_824AB578 at 0x824AB5A0).
The 4 instructions immediately preceding it are:
addi r3, r0, 10 ; privilege bit 10
bl XexCheckExecutablePrivilege
cmpli 0, r3, 0
bc 12, eq, 0x824AB724 ; if r3==0, skip whole block
- exports.rs:193 registers XexCheckExecutablePrivilege as
stub_return_zero. Always returning 0 -> guest takes the branch
and skips the entire AV/crypto/save-data init block.
- The other call site (sub_824A9710 at 0x824A99A0) queries privilege
11 with opposite polarity (bne) -> gates XamTaskSchedule on the
privilege-NOT-set arm. With both stubs returning 0, the guest
walks the wrong arm of every privilege-gated branch.
- This explains why the dispatcher fields read zero
([0x828F3D08+0x50]=0, [0x828F4070+0x24]=0 from AUDIT-004 dumps):
the ctors run, but the producers that would populate those fields
with a non-zero handle never execute.
Next session: replace XexCheckExecutablePrivilege stub with real
priv-bit lookup from XEX header. See audit-findings.md
KRNBUG-AUDIT-005 for the validation matrix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -184,8 +184,13 @@ enum Commands {
|
||||
/// pool-element `this` addresses that MSVC ctors fail to
|
||||
/// preserve in r31. Read-only; lockstep digest unaffected.
|
||||
/// Examples: `--ctor-probe=0x8217C850`,
|
||||
/// `--ctor-probe=0x82181750,0x821701C8`.
|
||||
#[arg(long)]
|
||||
/// `--ctor-probe=0x82181750,0x821701C8`. KRNBUG-AUDIT-005:
|
||||
/// each token may be extended to `PC@DISPATCHER:OFFSET` (e.g.
|
||||
/// `0x821802D8@0x828F3D08:80`); on a hit the helper additionally
|
||||
/// logs the value of `[DISPATCHER+OFFSET]` — what the producer's
|
||||
/// `lwz r3, OFFSET(r3)` is about to read. The flag is also
|
||||
/// reachable as `--pc-probe` (semantically clearer name).
|
||||
#[arg(long, alias = "pc-probe")]
|
||||
ctor_probe: Option<String>,
|
||||
/// Diagnostic. Comma-separated list of guest addresses to dump
|
||||
/// (64 bytes each, hex + u32 lanes) at end-of-run, after the
|
||||
@@ -916,27 +921,39 @@ fn cmd_exec_inner(
|
||||
// worker prologue checks this set on every step; on a hit it
|
||||
// prints a single back-chain capture line. Empty set = no
|
||||
// probes = no-op fast path.
|
||||
let ctor_probe_combined: Option<String> = match (ctor_probe, std::env::var("XENIA_CTOR_PROBE").ok()) {
|
||||
let ctor_probe_combined: Option<String> = match (
|
||||
ctor_probe,
|
||||
std::env::var("XENIA_CTOR_PROBE").ok().or_else(|| std::env::var("XENIA_PC_PROBE").ok()),
|
||||
) {
|
||||
(Some(s), _) => Some(s.to_string()),
|
||||
(None, Some(s)) if !s.is_empty() => Some(s),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(list) = ctor_probe_combined {
|
||||
for token in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let parsed = if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
|
||||
u32::from_str_radix(hex, 16)
|
||||
} else {
|
||||
token.parse::<u32>()
|
||||
let (pc_str, consumer): (&str, Option<(u32, u32)>) = match token.split_once('@') {
|
||||
None => (token, None),
|
||||
Some((pc_part, rest)) => {
|
||||
let (disp_str, off_str) = rest.split_once(':').ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"invalid --pc-probe token {token:?}: expected PC@DISPATCHER:OFFSET"
|
||||
)
|
||||
})?;
|
||||
let disp = parse_hex_u32(disp_str.trim()).map_err(|e| {
|
||||
anyhow::anyhow!("invalid dispatcher in {token:?}: {e}")
|
||||
})?;
|
||||
let off = parse_hex_u32(off_str.trim()).map_err(|e| {
|
||||
anyhow::anyhow!("invalid offset in {token:?}: {e}")
|
||||
})?;
|
||||
(pc_part.trim(), Some((disp, off)))
|
||||
}
|
||||
};
|
||||
match parsed {
|
||||
Ok(pc) => {
|
||||
kernel.ctor_probe_pcs.insert(pc);
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"invalid PC in --ctor-probe: {token:?}"
|
||||
));
|
||||
}
|
||||
let pc = parse_hex_u32(pc_str).map_err(|e| {
|
||||
anyhow::anyhow!("invalid PC in --pc-probe: {token:?}: {e}")
|
||||
})?;
|
||||
kernel.ctor_probe_pcs.insert(pc);
|
||||
if let Some(c) = consumer {
|
||||
kernel.pc_probe_consumers.insert(pc, c);
|
||||
}
|
||||
}
|
||||
if !quiet && !kernel.ctor_probe_pcs.is_empty() {
|
||||
@@ -946,9 +963,10 @@ fn cmd_exec_inner(
|
||||
.map(|p| format!("{p:#010x}"))
|
||||
.collect();
|
||||
tracing::info!(
|
||||
"ctor probes armed: {} ({})",
|
||||
"pc probes armed: {} ({}); consumer-snapshots: {}",
|
||||
kernel.ctor_probe_pcs.len(),
|
||||
pcs.join(", ")
|
||||
pcs.join(", "),
|
||||
kernel.pc_probe_consumers.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user