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:
MechaCat02
2026-05-03 21:14:56 +02:00
parent 6440261e2e
commit f84e947547
4 changed files with 636 additions and 7 deletions

View File

@@ -59,6 +59,14 @@ pub struct HandleAuditTrail {
/// from walking the PPC back-chain. An empty vec means either the
/// handle wasn't in focus or the create site didn't capture a stack.
pub created_stack: Vec<(u32, u32)>,
/// KRNBUG-AUDIT-003 class probes. Each entry is one already-formatted
/// "frame=N r31=0x... vtable=0x... class=..." line, captured at
/// allocation time from the live PPC context (frame 0: ctx.gpr[31] /
/// r30 / r3) and the standard prologue spill area at `[fp - 12]` /
/// `[fp - 16]` for deeper frames. Pre-formatted because the source
/// memory is overwritten once tid=1 leaves the static-init phase, so
/// the probe must run at the create call site, not at end-of-run.
pub created_class_probes: Vec<String>,
/// Bounded ring of signal events.
pub signals: VecDeque<HandleAuditEntry>,
/// Bounded ring of wait-entry events (one per `Wait*` call).
@@ -73,6 +81,7 @@ impl HandleAuditTrail {
kind,
created,
created_stack: Vec::new(),
created_class_probes: Vec::new(),
signals: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
waits: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
wakes: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
@@ -141,12 +150,30 @@ impl HandleAudit {
kind: &'static str,
entry: HandleAuditEntry,
stack: Vec<(u32, u32)>,
) {
self.record_create_with_stack_and_probes(handle, kind, entry, stack, Vec::new());
}
/// Variant of `record_create_with_stack` that also accepts pre-
/// formatted class-probe strings (KRNBUG-AUDIT-003). Each string is
/// one frame's RTTI/vtable readout: `frame=N candidate=r31 this=0x...
/// vtable=0x... class=...` or the RTTI-stripped fallback. Caller
/// formats them so this module remains memory-layout-agnostic.
#[inline]
pub fn record_create_with_stack_and_probes(
&mut self,
handle: u32,
kind: &'static str,
entry: HandleAuditEntry,
stack: Vec<(u32, u32)>,
class_probes: Vec<String>,
) {
if !self.enabled {
return;
}
let mut trail = HandleAuditTrail::new(kind, entry);
trail.created_stack = stack;
trail.created_class_probes = class_probes;
self.trails.insert(handle, trail);
}