feat(kernel): KRNBUG-AUDIT-003 — vtable/RTTI class probe at handle creation + wait
Adds a read-only MSVC RTTI traversal helper (`read_class_at_this`)
and a `probe_create_stack_classes` integration that walks each
captured back-chain frame for handle creates in `--trace-handles-focus`
and probes each frame's most-likely `this` candidate (live r31/r30/r3
for frame 0; saved-r31/r30 from the prologue spill area at [fp-12]/
[fp-16] for deeper frames). False-positive guard rejects the CRT
static-init iterator pattern (vtable's first two slots must be image-
range function pointers — PPC instruction words like `mflr r12` are
not in 0x82xxxxxx).
`dump_thread_diagnostic` now takes `&GuestMemory` so the FOCUS report
prints, for each parked waiter, a WAIT-THREAD block with full back-
chain frames and per-slot saved-register dump for offline lookup.
End-to-end finding (-n 500M producer-trace):
* Handle 0x100c dispatcher = 0x828F3D08 (image rdata; verified by
sub_82181750 disasm + xref table). [this+0] = -1 sentinel — POD
job queue, NOT a C++ polymorphic class.
* Handle 0x15e0 dispatcher = 0x828F4070 (same shape).
* Handle 0x1004's 8-instance pool members still TBD (MSVC ctors
didn't preserve `this` in r31).
* 0x42450b5c is a separate audit class (heap-allocated, parks via
non-`do_wait_single` path).
Decisive xref audit: every reference to 0x828F3D08 / 0x828F4070 in
the static analysis is in a ctor or the CRT init driver. NO producer
code references either dispatcher base. Confirms `signal_attempts=0`
is unreachable-producer, not broken-producer.
Tests: 581 → 586 green (+5: RTTI-intact / RTTI-stripped / non-object
/ cstring / probe_create_stack integration). `--stable-digest -n
100M` instructions=100000002 unchanged. Master HEAD prior: 6440261.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1279,7 +1279,7 @@ fn cmd_exec_inner(
|
||||
db.finalize_traces()?;
|
||||
}
|
||||
print_summary(kernel.scheduler.ctx(0), &debugger, &db_writer, quiet);
|
||||
dump_thread_diagnostic(&kernel, quiet);
|
||||
dump_thread_diagnostic(&kernel, &*mem_arc, quiet);
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
instructions = stats.instruction_count,
|
||||
@@ -2968,7 +2968,11 @@ fn print_summary(
|
||||
/// session see *which* thread is stuck *where* without having to re-derive
|
||||
/// it from ring-buffer / event-count math. Mirrors the halt-on-deadlock
|
||||
/// dump in `run_execution` but runs on the normal `-n` exit path too.
|
||||
fn dump_thread_diagnostic(kernel: &xenia_kernel::KernelState, quiet: bool) {
|
||||
fn dump_thread_diagnostic(
|
||||
kernel: &xenia_kernel::KernelState,
|
||||
mem: &xenia_memory::GuestMemory,
|
||||
quiet: bool,
|
||||
) {
|
||||
if quiet {
|
||||
return;
|
||||
}
|
||||
@@ -3216,6 +3220,15 @@ fn dump_thread_diagnostic(kernel: &xenia_kernel::KernelState, quiet: bool) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if !t.created_class_probes.is_empty() {
|
||||
println!(
|
||||
" created-class probes ({}):",
|
||||
t.created_class_probes.len(),
|
||||
);
|
||||
for line in &t.created_class_probes {
|
||||
println!(" {}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Producer-class classification. Source strings are stable
|
||||
// labels passed to `record_signal` at the export sites.
|
||||
@@ -3299,6 +3312,96 @@ fn dump_thread_diagnostic(kernel: &xenia_kernel::KernelState, quiet: bool) {
|
||||
"not stuck — signals consumed correctly"
|
||||
};
|
||||
println!(" => {}", conclusion);
|
||||
|
||||
// KRNBUG-AUDIT-003. For each thread parked on this focus
|
||||
// handle, walk its back-chain and probe `this` candidates
|
||||
// for an MSVC C++ class name via vtable[-4] → COL → TD.
|
||||
// Frame 0 uses the live r31/r30/r3 of the parked thread;
|
||||
// deeper frames recover saved r31/r30 from the standard
|
||||
// PPC prologue spill area at [fp - 12] / [fp - 16]. This
|
||||
// names the dispatcher / queue / pool that owns each
|
||||
// parked thread, converting "who should signal handle X"
|
||||
// into "who should call Class::Submit but doesn't."
|
||||
if let Some(obj) = kernel.objects.get(&h) {
|
||||
let waiters: Vec<xenia_cpu::ThreadRef> = match obj {
|
||||
KernelObject::Event { waiters, .. }
|
||||
| KernelObject::Semaphore { waiters, .. }
|
||||
| KernelObject::Timer { waiters, .. }
|
||||
| KernelObject::Thread { waiters, .. }
|
||||
| KernelObject::Mutex { waiters, .. } => waiters.iter().copied().collect(),
|
||||
KernelObject::File { .. } => Vec::new(),
|
||||
};
|
||||
for w in waiters {
|
||||
let Some(slot) = kernel.scheduler.slots.get(w.hw_id as usize) else {
|
||||
continue;
|
||||
};
|
||||
let Some(thread) = slot.runqueue.get(w.idx as usize) else {
|
||||
continue;
|
||||
};
|
||||
let ctx = &thread.ctx;
|
||||
let frames = xenia_kernel::state::walk_guest_back_chain(
|
||||
ctx.gpr[1] as u32,
|
||||
ctx.lr as u32,
|
||||
mem,
|
||||
6,
|
||||
);
|
||||
println!(
|
||||
" WAIT-THREAD tid={} pc={:#010x} lr={:#010x} sp={:#010x} r3={:#010x} r30={:#010x} r31={:#010x}",
|
||||
thread.tid,
|
||||
ctx.pc,
|
||||
ctx.lr as u32,
|
||||
ctx.gpr[1] as u32,
|
||||
ctx.gpr[3] as u32,
|
||||
ctx.gpr[30] as u32,
|
||||
ctx.gpr[31] as u32,
|
||||
);
|
||||
for (frame_idx, (fp, frame_lr)) in frames.iter().enumerate() {
|
||||
let saved_r31 = mem.read_u32(fp.wrapping_sub(12));
|
||||
let saved_r30 = mem.read_u32(fp.wrapping_sub(16));
|
||||
let saved_r29 = mem.read_u32(fp.wrapping_sub(20));
|
||||
let saved_r28 = mem.read_u32(fp.wrapping_sub(24));
|
||||
println!(
|
||||
" FRAME {} fp={:#010x} lr={:#010x} saved r31={:#010x} r30={:#010x} r29={:#010x} r28={:#010x}",
|
||||
frame_idx, fp, frame_lr, saved_r31, saved_r30, saved_r29, saved_r28,
|
||||
);
|
||||
let candidates: Vec<(u32, &'static str)> = if frame_idx == 0 {
|
||||
vec![
|
||||
(ctx.gpr[31] as u32, "r31"),
|
||||
(ctx.gpr[30] as u32, "r30"),
|
||||
(ctx.gpr[29] as u32, "r29"),
|
||||
(ctx.gpr[28] as u32, "r28"),
|
||||
(ctx.gpr[3] as u32, "r3"),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
(saved_r31, "saved-r31"),
|
||||
(saved_r30, "saved-r30"),
|
||||
(saved_r29, "saved-r29"),
|
||||
(saved_r28, "saved-r28"),
|
||||
]
|
||||
};
|
||||
for (this_ptr, label) in candidates {
|
||||
use xenia_kernel::state::ClassReadout;
|
||||
match xenia_kernel::state::read_class_at_this(this_ptr, mem) {
|
||||
ClassReadout::Named { vtable, mangled } => {
|
||||
println!(
|
||||
" WAIT-CLASS frame={} {}={:#010x} vtable={:#010x} class={}",
|
||||
frame_idx, label, this_ptr, vtable, mangled,
|
||||
);
|
||||
}
|
||||
ClassReadout::VtableOnly { vtable, virtuals } => {
|
||||
println!(
|
||||
" WAIT-CLASS frame={} {}={:#010x} vtable={:#010x} virtuals=[{:#010x},{:#010x},{:#010x},{:#010x}] (RTTI stripped)",
|
||||
frame_idx, label, this_ptr, vtable,
|
||||
virtuals[0], virtuals[1], virtuals[2], virtuals[3],
|
||||
);
|
||||
}
|
||||
ClassReadout::NotAnObject => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3337,7 +3440,7 @@ fn run_with_ui(
|
||||
let worker_span = tracing::info_span!("cpu_worker");
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("xenia-cpu".into())
|
||||
.spawn(move || -> Result<(ExecStats, xenia_kernel::KernelState, xenia_debugger::Debugger, Option<xenia_analysis::DbWriter>)> {
|
||||
.spawn(move || -> Result<(ExecStats, xenia_memory::GuestMemory, xenia_kernel::KernelState, xenia_debugger::Debugger, Option<xenia_analysis::DbWriter>)> {
|
||||
let _guard = worker_span.enter();
|
||||
let stats = run_execution(
|
||||
&mut mem,
|
||||
@@ -3354,7 +3457,7 @@ fn run_with_ui(
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.finalize_traces()?;
|
||||
}
|
||||
Ok((stats, kernel, debugger, db_writer))
|
||||
Ok((stats, mem, kernel, debugger, db_writer))
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!("spawn CPU worker: {e}"))?;
|
||||
|
||||
@@ -3362,7 +3465,7 @@ fn run_with_ui(
|
||||
// flips the shutdown flag itself (e.g. after max_instructions).
|
||||
xenia_ui::run(event_loop, ui_handles, &title_owned)?;
|
||||
|
||||
let (stats, kernel, debugger, db_writer) = match worker.join() {
|
||||
let (stats, mem, kernel, debugger, db_writer) = match worker.join() {
|
||||
Ok(res) => res?,
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!("CPU worker thread panicked"));
|
||||
@@ -3370,7 +3473,7 @@ fn run_with_ui(
|
||||
};
|
||||
|
||||
print_summary(kernel.scheduler.ctx(0), &debugger, &db_writer, quiet);
|
||||
dump_thread_diagnostic(&kernel, quiet);
|
||||
dump_thread_diagnostic(&kernel, &mem, quiet);
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
instructions = stats.instruction_count,
|
||||
|
||||
Reference in New Issue
Block a user