1 Commits

Author SHA1 Message Date
MechaCat02
229b46c765 [Kernel] Slab-recycle handle allocator (AUDIT-059 R34)
Adds a FIFO free list of closed handle slots so alloc_handle returns
recycled IDs before bumping next_handle. Mirrors canary's slab-style
ObjectTable: F8000098 reused 130x per 30s window in canary, but ours'
monotonic bump allocator never reused slots — so a recycled slot in
canary maps to a fresh, never-reused slot in ours, drifting kernel
object identity per AUDIT-042's analysis.

release_handle_slot is wired into nt_close's refcount==0 branch and
gated to the canonical [0x1000, 0xF000_0000) range so synthetic
XAudio park handles (AUDIT-048) are never recycled.

Verified: all 655 workspace tests green, smoke tests at -n 50M show
NtClose 115/run with handle table renumbering active (round-34 max
handle 0x12ac vs round-16 baseline 0x12b8 over same workload). γ-
cluster #2 wedge unchanged — silph wait still parks tid=13 on the
renumbered handle (4216=0x1078 here vs 0x12a4 baseline), confirming
the wedge is independent of allocator policy. Lands as a parity
fix to bring our kernel-object identity in line with canary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 18:04:34 +02:00
2 changed files with 33 additions and 0 deletions

View File

@@ -1977,6 +1977,9 @@ fn nt_close(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) {
// so a later scheduler round doesn't try to signal a dead handle.
// `disarm_timer` is a no-op for non-timer handles.
state.disarm_timer(handle);
// AUDIT-059 R34: return the slot to the recycle FIFO so a later
// `alloc_handle` mints the same ID (matching canary's slab).
state.release_handle_slot(handle);
}
ctx.gpr[3] = 0;
}

View File

@@ -56,6 +56,18 @@ pub struct KernelState {
/// publish; observers (the kernel object table) are guarded by
/// their own synchronization.
next_handle: std::sync::atomic::AtomicU32,
/// AUDIT-059 R34: FIFO free list of closed handle slots, mirroring
/// canary's slab/free-list `ObjectTable`. Without this, ours' bump
/// allocator monotonically grows so a recycled slot in canary
/// (e.g. `F8000098` reused 130× per 30s) corresponds to a fresh,
/// never-reused slot in ours — the kernel-object identity drifts.
/// Recycling closes that gap and (per AUDIT-042 / R30) may
/// side-effect-unwedge γ-cluster #2 by letting silph signals land
/// on the same handle slot the wait registered for. Population is
/// gated on `KernelState::release_handle_slot` (only IDs in
/// `[HANDLE_BASE, 0xF000_0000)` are recycled — synthetic XAudio
/// handles at `0xF000_0000+` are reserved and must never be reused).
free_handles: std::collections::VecDeque<u32>,
/// Scheduler managing all emulated HW threads + their per-slot
/// runqueues. Starts empty — the app installs the initial guest thread
/// on slot 0 via `KernelState::install_initial_thread` once it has the
@@ -338,6 +350,7 @@ impl KernelState {
let mut state = Self {
exports: HashMap::new(),
next_handle: AtomicU32::new(0x1000),
free_handles: std::collections::VecDeque::new(),
scheduler,
next_tls_index: AtomicU32::new(0),
cs_waiters: HashMap::new(),
@@ -660,12 +673,29 @@ impl KernelState {
}
pub fn alloc_handle(&mut self) -> u32 {
// AUDIT-059 R34: prefer recycling a closed slot (FIFO, matching
// canary's `ObjectTable` slab) before bumping. The Arc<Mutex<
// KernelState>> already serializes us; no extra synchronization.
if let Some(slot) = self.free_handles.pop_front() {
return slot;
}
// M2.4: lock-free fetch_add. Relaxed is sufficient — IDs are
// opaque tokens; no payload is sequenced against the counter.
self.next_handle
.fetch_add(4, std::sync::atomic::Ordering::Relaxed)
}
/// AUDIT-059 R34. Return a freshly-closed handle slot to the FIFO
/// recycle queue. No-op for the synthetic XAudio range (`>= 0xF000_0000`,
/// AUDIT-048) and the reserved `< 0x1000` band. Call site: `nt_close`'s
/// `objects.remove` branch when refcount reaches zero.
pub fn release_handle_slot(&mut self, handle: u32) {
if handle < 0x1000 || handle >= 0xF000_0000 {
return;
}
self.free_handles.push_back(handle);
}
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
let h = self.alloc_handle();
self.objects.insert(h, obj);