[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>
This commit is contained in:
@@ -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.
|
// so a later scheduler round doesn't try to signal a dead handle.
|
||||||
// `disarm_timer` is a no-op for non-timer handles.
|
// `disarm_timer` is a no-op for non-timer handles.
|
||||||
state.disarm_timer(handle);
|
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;
|
ctx.gpr[3] = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ pub struct KernelState {
|
|||||||
/// publish; observers (the kernel object table) are guarded by
|
/// publish; observers (the kernel object table) are guarded by
|
||||||
/// their own synchronization.
|
/// their own synchronization.
|
||||||
next_handle: std::sync::atomic::AtomicU32,
|
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
|
/// Scheduler managing all emulated HW threads + their per-slot
|
||||||
/// runqueues. Starts empty — the app installs the initial guest thread
|
/// runqueues. Starts empty — the app installs the initial guest thread
|
||||||
/// on slot 0 via `KernelState::install_initial_thread` once it has the
|
/// on slot 0 via `KernelState::install_initial_thread` once it has the
|
||||||
@@ -338,6 +350,7 @@ impl KernelState {
|
|||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
exports: HashMap::new(),
|
exports: HashMap::new(),
|
||||||
next_handle: AtomicU32::new(0x1000),
|
next_handle: AtomicU32::new(0x1000),
|
||||||
|
free_handles: std::collections::VecDeque::new(),
|
||||||
scheduler,
|
scheduler,
|
||||||
next_tls_index: AtomicU32::new(0),
|
next_tls_index: AtomicU32::new(0),
|
||||||
cs_waiters: HashMap::new(),
|
cs_waiters: HashMap::new(),
|
||||||
@@ -660,12 +673,29 @@ impl KernelState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn alloc_handle(&mut self) -> u32 {
|
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
|
// M2.4: lock-free fetch_add. Relaxed is sufficient — IDs are
|
||||||
// opaque tokens; no payload is sequenced against the counter.
|
// opaque tokens; no payload is sequenced against the counter.
|
||||||
self.next_handle
|
self.next_handle
|
||||||
.fetch_add(4, std::sync::atomic::Ordering::Relaxed)
|
.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 {
|
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
|
||||||
let h = self.alloc_handle();
|
let h = self.alloc_handle();
|
||||||
self.objects.insert(h, obj);
|
self.objects.insert(h, obj);
|
||||||
|
|||||||
Reference in New Issue
Block a user