From 5aaadfec361560deea01f19caae55054a0a72a00 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 12 Jun 2026 20:29:01 +0200 Subject: [PATCH] [iterate-2E] Add XENIA_AUDIT_DEREF pointer-chase probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On each AUDIT-PC-PROBE fire, treat gpr[reg] as a base object, dump its first 64 bytes, follow [base+off] to a sub-object, dump that, then follow [[base+off]+0] to its vtable and dump 48 slots. Env-gated (XENIA_AUDIT_DEREF=:), read-only, lockstep digest unaffected. Captures the live work-item + stream object + vtable at sub_824510E0 before the pool recycles the slot — which overturned the prior session's "infinite spin" diagnosis: the streaming read PROGRESSES 68/68 128KB chunks of a 9MB file, then the hub (tid=5) blocks INFINITE on a self-created Event/Manual (0x1060) that is never signaled. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/xenia-app/src/main.rs | 23 +++++++++++++++++ crates/xenia-kernel/src/state.rs | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index e4765db..1277528 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -1301,6 +1301,29 @@ fn cmd_exec_inner( } } + // iterate-2E — pointer-chase probe. `XENIA_AUDIT_DEREF=:` + // (e.g. `4:36`). On each AUDIT-PC-PROBE fire, dumps gpr[reg] as a base + // object, the sub-object at [base+off], and that sub-object's vtable. + // Read-only; lockstep digest unaffected. + if let Ok(spec) = std::env::var("XENIA_AUDIT_DEREF") { + if !spec.is_empty() { + let (rs, os) = spec + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("XENIA_AUDIT_DEREF {spec:?}: expected :"))?; + let reg: u8 = rs.trim_start_matches('r').parse() + .map_err(|e| anyhow::anyhow!("XENIA_AUDIT_DEREF reg {rs:?}: {e}"))?; + let off: u32 = if let Some(h) = os.strip_prefix("0x") { + u32::from_str_radix(h, 16) + } else { + os.parse::() + }.map_err(|e| anyhow::anyhow!("XENIA_AUDIT_DEREF off {os:?}: {e}"))?; + kernel.audit_deref = Some((reg, off)); + if !quiet { + tracing::info!("audit-deref armed: r{} +0x{:x}", reg, off); + } + } + } + // 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 diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 9ccf0ed..e34b065 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -291,6 +291,17 @@ pub struct KernelState { /// Settable via `--audit-r3-dump-bytes` / /// `XENIA_AUDIT_R3_DUMP_BYTES`. pub audit_r3_dump_bytes: Option, + /// iterate-2E — diagnostic pointer-chase. `(reg, off)`: on every + /// `AUDIT-PC-PROBE` fire, treat `gpr[reg]` as a base object pointer, + /// dump its first 64 bytes, then follow `[base+off]` to a sub-object + /// (e.g. a stream/file object held in a work item), dump ITS first 64 + /// bytes, then follow `[[base+off]+0]` to the sub-object's vtable and + /// dump the first 48 u32 slots. Designed to capture the live work-item + /// + stream object + vtable at `sub_824510E0` entry (r4 = work item, + /// stream at +36, vtable[28] = the "is-read-done?" predicate) BEFORE + /// the pool recycles the slot. Read-only; lockstep digest unaffected. + /// Settable via `XENIA_AUDIT_DEREF=:` (e.g. `4:36`). + pub audit_deref: Option<(u8, 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 @@ -418,6 +429,7 @@ impl KernelState { audit_pc_probe_pcs: std::collections::HashSet::new(), audit_mem_read_addr: None, audit_r3_dump_bytes: None, + audit_deref: None, lr_trace_pcs: std::collections::HashSet::new(), lr_trace_writer: None, dump_addrs: Vec::new(), @@ -1124,6 +1136,38 @@ impl KernelState { } println!("{}", out); } + // iterate-2E — pointer-chase: dump base object (gpr[reg]), the + // sub-object it holds at [base+off], and that sub-object's vtable + // slots. Captures the live work-item + stream + vtable[28] at + // sub_824510E0 before the pool recycles the slot. Read-only. + if let Some((reg, deref_off)) = self.audit_deref { + use std::fmt::Write as _; + let base = ctx.gpr[reg as usize] as u32; + let dump64 = |label: &str, p: u32| { + let mut s = String::with_capacity(256); + let _ = write!(&mut s, "AUDIT-DEREF {} ptr={:#010x}", label, p); + let mut o: u32 = 0; + while o < 64 { + let _ = write!(&mut s, " +0x{:02x}={:#010x}", o, mem.read_u32(p.wrapping_add(o))); + o += 4; + } + println!("{}", s); + }; + println!("AUDIT-DEREF-HEAD pc={:#010x} tid={} cycle={} reg=r{} off=0x{:x}", pc, tid, cycle, reg, deref_off); + dump64("item", base); + let sub = mem.read_u32(base.wrapping_add(deref_off)); + dump64("sub", sub); + let vt = mem.read_u32(sub); // [sub+0] = vtable + // Dump 48 vtable slots so slot 28 (+0x70) and slot 36 (+0x90) show. + let mut s = String::with_capacity(512); + let _ = write!(&mut s, "AUDIT-DEREF vtable={:#010x}", vt); + let mut slot: u32 = 0; + while slot < 48 { + let _ = write!(&mut s, " [{}]={:#010x}", slot, mem.read_u32(vt.wrapping_add(slot * 4))); + slot += 1; + } + println!("{}", s); + } } /// M12 — diagnostic. If the live PC for HW slot `hw_id` is in