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

@@ -455,6 +455,41 @@ impl KernelState {
self.audit.record_create(handle, kind, entry);
}
/// KRNBUG-AUDIT-002. Variant of `audit_create` that additionally
/// captures a 6-frame guest stack trace at allocation time when the
/// handle is in `audit.focus`. Outside the focus set this falls back
/// to plain `audit_create` (no stack walk → no extra cost on the hot
/// path of unfocused handle creation).
///
/// The walk reads the PPC EABI back-chain: `[r1] = prev_sp`, and the
/// LR saved by *that* prev frame's prologue lives at `[prev_sp - 8]`.
/// Frame 0 is the live frame `(ctx.gpr[1], ctx.lr)`. Frames 1..N walk
/// upward. A read returning 0 / 0xFFFF_FFFF, or a self-loop, ends the
/// walk early. This is read-only — guest memory and CPU state are not
/// mutated, so lockstep determinism is unaffected (a parallel run with
/// no focus is byte-identical to one without this code path).
pub fn audit_create_with_ctx(
&mut self,
handle: u32,
kind: &'static str,
ctx: &PpcContext,
mem: &GuestMemory,
source: &'static str,
) {
if !self.audit.enabled {
return;
}
let lr = ctx.lr as u32;
let entry = self.audit_entry(lr, source, 0);
if !self.audit.focus.contains(&handle) {
self.audit.record_create(handle, kind, entry);
return;
}
let stack = walk_guest_back_chain(ctx.gpr[1] as u32, lr, mem, 6);
self.audit
.record_create_with_stack(handle, kind, entry, stack);
}
/// Record a Set/Pulse/Release/etc. call against a handle. `aux` is the
/// previous signal state (or per-export-specific data).
pub fn audit_signal(&mut self, handle: u32, lr: u32, source: &'static str, aux: u64) {
@@ -675,6 +710,47 @@ impl Default for KernelState {
}
}
/// Walk the PPC EABI back-chain starting from `sp` (the value in r1 at
/// the moment of capture). Returns up to `max_frames` entries of
/// `(frame_pointer, saved_lr)`. Index 0 is the live frame
/// `(sp, live_lr)` — `live_lr` is the caller-supplied current LR, since
/// it has not yet been spilled to memory by this frame's prologue.
///
/// PPC convention reminder: a function's prologue stores the caller's
/// LR at `[old_sp - 8]` *before* bumping `r1` down to the new frame. So
/// from the live `sp`, `prev_sp = mem[sp]` and the LR saved in the
/// frame above is at `mem[prev_sp - 8]`. The walk stops on a
/// 0/0xFFFFFFFF/self-loop sentinel — those guard against
/// uninitialized stacks and the topmost frame.
///
/// This is read-only; it never mutates guest memory or CPU state.
pub fn walk_guest_back_chain(
sp: u32,
live_lr: u32,
mem: &GuestMemory,
max_frames: usize,
) -> Vec<(u32, u32)> {
let mut frames = Vec::with_capacity(max_frames);
if max_frames == 0 {
return frames;
}
frames.push((sp, live_lr));
let mut cur = sp;
while frames.len() < max_frames {
if cur == 0 || cur == 0xFFFF_FFFF {
break;
}
let prev = mem.read_u32(cur);
if prev == 0 || prev == 0xFFFF_FFFF || prev == cur {
break;
}
let saved_lr = mem.read_u32(prev.wrapping_sub(8));
frames.push((prev, saved_lr));
cur = prev;
}
frames
}
#[cfg(test)]
mod tests {
use super::*;
@@ -759,4 +835,64 @@ mod tests {
);
assert!(set.iter().all(|h| (h - 0x1000) % 4 == 0));
}
/// KRNBUG-AUDIT-002: synthesize a 3-level back-chain in mapped guest
/// memory and walk it. Verifies that frame 0 is the live-LR frame and
/// that subsequent frames pull `prev_sp` from `[sp]` and the saved LR
/// from `[prev_sp - 8]`.
#[test]
fn back_chain_walker_resolves_synthetic_frames() {
let mem = GuestMemory::new().expect("memory init");
let mut state = KernelState::new();
let base = state.heap_alloc(0x4000, &mem).expect("scratch");
// Lay out three frames inside the scratch page. Each frame gets
// its own 0x100-byte slot. Frame N's `[sp + 0]` points at frame
// N+1's sp, and frame N+1's `[sp - 8]` holds the LR saved by
// that frame for the call into frame N.
let sp0 = base + 0x100;
let sp1 = base + 0x300;
let sp2 = base + 0x500;
// Back-chain pointers
mem.write_u32(sp0, sp1);
mem.write_u32(sp1, sp2);
mem.write_u32(sp2, 0); // top of stack
// Saved LRs (the LR of the call that reached the *next* frame
// up are stored at the next frame's sp - 8)
mem.write_u32(sp1.wrapping_sub(8), 0xAAAA_BBBB);
mem.write_u32(sp2.wrapping_sub(8), 0xCCCC_DDDD);
let frames = walk_guest_back_chain(sp0, 0x1111_2222, &mem, 6);
assert_eq!(frames.len(), 3);
assert_eq!(frames[0], (sp0, 0x1111_2222));
assert_eq!(frames[1], (sp1, 0xAAAA_BBBB));
assert_eq!(frames[2], (sp2, 0xCCCC_DDDD));
}
/// Walker must not loop on a self-referential back-chain (a corrupted
/// frame where `[sp] == sp`).
#[test]
fn back_chain_walker_stops_on_self_loop() {
let mem = GuestMemory::new().expect("memory init");
let mut state = KernelState::new();
let base = state.heap_alloc(0x1000, &mem).expect("scratch");
let sp = base + 0x100;
mem.write_u32(sp, sp); // self-loop
let frames = walk_guest_back_chain(sp, 0x4242_4242, &mem, 6);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0], (sp, 0x4242_4242));
}
/// Walker must terminate on the standard top-of-stack sentinel
/// (`[sp] == 0`) without spilling a bogus frame.
#[test]
fn back_chain_walker_stops_on_zero_sentinel() {
let mem = GuestMemory::new().expect("memory init");
let mut state = KernelState::new();
let base = state.heap_alloc(0x1000, &mem).expect("scratch");
let sp = base + 0x100;
mem.write_u32(sp, 0);
let frames = walk_guest_back_chain(sp, 0x8242_0000, &mem, 6);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0], (sp, 0x8242_0000));
}
}