From 229b46c76577656554ed7b5aec66d37a8247b4f7 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 10 Jun 2026 18:04:34 +0200 Subject: [PATCH] [Kernel] Slab-recycle handle allocator (AUDIT-059 R34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/xenia-kernel/src/exports.rs | 3 +++ crates/xenia-kernel/src/state.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index 91f1fdd..fab1ebe 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -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; } diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 0b0d23d..ac373cd 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -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, /// 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> 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);