feat(kernel): KRNBUG-AUDIT-002 — multi-frame guest stack capture at handle creation

Adds `walk_guest_back_chain` (PPC EABI back-chain walker) and a
`record_create_with_stack` audit hook gated on `--trace-handles-focus`.
NtCreateEvent / NtCreateSemaphore / NtCreateTimer / XamTaskSchedule now
route through the new helper so focused handles capture up to 6 stack
frames at allocation time. Diagnostic-only, read-only memory access:
unfocused handles pay one HashSet lookup, focused ones pay six
back-chain dereferences. Lockstep determinism preserved.

End-to-end finding: handles 0x1004 (8-instance pool via static ctor at
0x8280F810), 0x100c (singleton built inside main()), 0x15e0 (singleton
in distinct cluster) are silph-framework dispatcher objects whose
producer code is unreached at -n 500M. The producer hunt now has class
ownership; vtable/RTTI readout is the next step.

Tests: 576 → 581 green. `--stable-digest -n 100M` instructions=100000002
unchanged. Master HEAD prior: 9d45efe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-03 20:41:06 +02:00
parent 9d45efe5d5
commit 2a9fd1fc86
6 changed files with 403 additions and 7 deletions

View File

@@ -51,6 +51,14 @@ pub struct HandleAuditTrail {
pub kind: &'static str,
/// When/who/where the handle was minted.
pub created: HandleAuditEntry,
/// KRNBUG-AUDIT-002 producer-trace. Captured frames at allocation
/// time, only populated when the handle is in `HandleAudit::focus`
/// AND the create site routed through the `_with_stack` variant.
/// Frame layout: `(frame_pointer, saved_lr_for_caller_of_that_frame)`.
/// Index 0 is the live frame: `(ctx.gpr[1], ctx.lr)`. Index 1+ comes
/// 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)>,
/// Bounded ring of signal events.
pub signals: VecDeque<HandleAuditEntry>,
/// Bounded ring of wait-entry events (one per `Wait*` call).
@@ -64,6 +72,7 @@ impl HandleAuditTrail {
Self {
kind,
created,
created_stack: Vec::new(),
signals: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
waits: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
wakes: VecDeque::with_capacity(AUDIT_RING_CAPACITY),
@@ -121,6 +130,26 @@ impl HandleAudit {
.insert(handle, HandleAuditTrail::new(kind, entry));
}
/// Same as `record_create`, but additionally stores a captured guest
/// stack trace on the trail (`created_stack`). Intended for handles
/// in `focus` so the dump can name the actual subsystem caller of the
/// kernel API rather than just the immediate wrapper return.
#[inline]
pub fn record_create_with_stack(
&mut self,
handle: u32,
kind: &'static str,
entry: HandleAuditEntry,
stack: Vec<(u32, u32)>,
) {
if !self.enabled {
return;
}
let mut trail = HandleAuditTrail::new(kind, entry);
trail.created_stack = stack;
self.trails.insert(handle, trail);
}
#[inline]
pub fn record_signal(&mut self, handle: u32, entry: HandleAuditEntry) {
if !self.enabled {
@@ -268,4 +297,34 @@ mod tests {
let a = HandleAudit::default();
assert!(a.counts(0x10FC).is_none());
}
#[test]
fn create_with_stack_stores_frames() {
let mut a = HandleAudit { enabled: true, ..HandleAudit::default() };
let frames = vec![
(0x7000_0100, 0x824a_9f6c),
(0x7000_0200, 0x824a_b020),
(0x7000_0300, 0x82bb_aa00),
];
a.record_create_with_stack(
0x1004,
"Event/Manual",
entry(0, "NtCreateEvent"),
frames.clone(),
);
let trail = &a.trails[&0x1004];
assert_eq!(trail.created_stack, frames);
}
#[test]
fn create_with_stack_disabled_is_noop() {
let mut a = HandleAudit::default();
a.record_create_with_stack(
0x1004,
"Event/Manual",
entry(0, "NtCreateEvent"),
vec![(0x7000_0000, 0x8200_0000)],
);
assert!(a.trails.is_empty());
}
}