forge_alloc/layout/generational.rs
1//! `GenerationalSlab<T, B, G>` — typed allocator that returns stable
2//! [`Handle<T, G>`] values instead of raw pointers. Each slot carries a
3//! generation counter; a handle is only valid while its generation matches
4//! the slot's. This prevents ABA / use-after-free at the API level: a stale
5//! handle returns `None` rather than producing undefined behavior.
6//!
7//! Interleaved layout: generation + state live together in a single
8//! `GenerationalSlot<T>` struct, backed by one contiguous allocation.
9//! One cache-line load covers both the generation check and the value access.
10//!
11//! See `docs/ARCHITECTURE.md` for the generational-slab design.
12
13use core::marker::PhantomData;
14use core::ptr::NonNull;
15
16use forge_alloc_core::{AllocError, Allocator, FixedRange, NonZeroLayout};
17
18/// Width of the generation counter. Public so callers can pick `u32`
19/// (default — 2^32 reuses per slot before wraparound) or `u64` (effectively
20/// unbounded). Internal trait sealed via the empty trait pattern.
21pub trait GenerationInt: Copy + Eq + sealed::Sealed {
22 /// Initial generation value.
23 const ZERO: Self;
24 /// Increment, wrapping at the type's max value.
25 fn wrapping_inc(self) -> Self;
26}
27
28mod sealed {
29 pub trait Sealed {}
30 impl Sealed for u32 {}
31 impl Sealed for u64 {}
32}
33
34impl GenerationInt for u32 {
35 const ZERO: Self = 0;
36 #[inline]
37 fn wrapping_inc(self) -> Self {
38 self.wrapping_add(1)
39 }
40}
41
42impl GenerationInt for u64 {
43 const ZERO: Self = 0;
44 #[inline]
45 fn wrapping_inc(self) -> Self {
46 self.wrapping_add(1)
47 }
48}
49
50/// Stable, non-pointer handle to a slot in a [`GenerationalSlab`].
51///
52/// `Copy` (cheap to pass), `Eq` + `Hash` (works in maps), and notably does
53/// NOT carry a reference to the slab — handles do not extend the slab's
54/// lifetime. A handle outliving its slab is allowed and gives `None` on
55/// access.
56///
57/// # Generation wraparound — closed by slot retirement
58///
59/// The generation counter increments on every `remove` of a slot and is
60/// `wrapping_add(1)` (per `GenerationInt`). Without mitigation, after `2^G`
61/// reuses of the *same* slot the counter would return to its original value
62/// and a stale `Handle` could be re-accepted against a different `T` (the
63/// classic ABA problem at a long horizon).
64///
65/// This is **defended, not merely documented**: when a slot's generation
66/// wraps a full cycle (the increment reaches `ZERO`), `remove` **retires**
67/// the slot — it is never returned to the freelist, so it can never be
68/// re-issued at a generation a stale handle holds. The cost is one leaked
69/// slot per full wrap of that slot (4.3 billion removes for `u32`, which is
70/// negligible). The choice between `u32` and `u64` is therefore now about how
71/// soon a hot slot is retired, not about a UAF window:
72///
73/// - `G = u32` (default): wrap after **2^32 ≈ 4.3 billion** reuses of
74/// the same slot. Realistic for long-running servers with high churn
75/// on a small slab (e.g. a 1024-slot connection pool processing
76/// millions of connections per second).
77/// - `G = u64`: wrap after 2^64 reuses — effectively unreachable in
78/// any realistic deployment (>500 years at 1 GHz of pure slot churn).
79/// Recommended for long-lived servers; the per-handle cost is 8
80/// extra bytes (8 bytes for `u32`, 16 for `u64` — the wider counter
81/// forces 8-byte struct alignment and 4 bytes of tail padding).
82///
83/// `Copy` means a handle can outlive the slot's original lifetime
84/// arbitrarily — including past 2^G recycles. If your handles can
85/// realistically be stashed for that long (audit logs, persisted
86/// session records, snapshot indices), use `Handle<T, u64>`. For
87/// per-request handles that never escape the request scope, `u32` is
88/// fine.
89///
90/// # Known limitation — cross-pool handle confusion
91///
92/// `Handle<T, G>` is typed by `T` (and `G`) but **not** by which
93/// `GenerationalSlab<T, B, G>` issued it. Passing a handle returned
94/// by `pool_a.insert(...)` to `pool_b.get(...)` is currently a
95/// runtime concern, not a compile error: the slot index will be
96/// interpreted against `pool_b`'s slot array, and ABA-safety
97/// degenerates to "match-by-coincidence" on the generation counter.
98///
99/// Closing this gap requires runtime-unique branding — either an
100/// invariant-lifetime tag (`generativity` crate style) or a
101/// monotonic pool-id passed through `PhantomData` — and is
102/// **API-breaking** because every `Handle<T, G>` user signature
103/// would gain an extra type parameter. The v0.1 API ships without
104/// it; v2.0+ may revisit. See the "Generational-handle slab" recipe
105/// in `docs/COMPOSITION_RECIPES.md` for the documented pitfall.
106///
107/// Until branded, callers who keep multiple pools of the same `T`
108/// must NOT mix their handles. The naming convention recommended in
109/// the recipes doc is to type-alias each pool's handle:
110/// `type SessionHandle = Handle<Session, u32>` per pool, in
111/// different modules.
112#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
113pub struct Handle<T, G: GenerationInt = u32> {
114 index: u32,
115 generation: G,
116 _phantom: PhantomData<*const T>,
117}
118
119/// One slot in the generational slab — generation + value state colocated.
120enum SlotState<T> {
121 Occupied(T),
122 Free { next_free: Option<u32> },
123}
124
125struct GenerationalSlot<T, G: GenerationInt> {
126 generation: G,
127 state: SlotState<T>,
128}
129
130/// Generational slab.
131///
132/// `capacity` slots, each holding either a live `T` or a free-list link.
133/// Handles encode `(slot_index, generation_at_allocate)`; access via
134/// [`get`](Self::get) / [`get_mut`](Self::get_mut) / [`remove`](Self::remove)
135/// only succeeds when the slot's current generation equals the handle's.
136///
137/// # Thread safety
138///
139/// All mutation goes through `&mut self`. The slab is `Send + Sync` if `T`
140/// and `B` are, but concurrent mutation requires external synchronization
141/// (`Mutex<GenerationalSlab<T, B>>`).
142pub struct GenerationalSlab<T, B: Allocator + FixedRange, G: GenerationInt = u32> {
143 backing: B,
144 /// Byte offset from `backing.base()` to the start of the slot array.
145 ///
146 /// We deliberately do NOT store an absolute `NonNull<GenerationalSlot<T, G>>`
147 /// captured from `backing.allocate(...)` at construction. Backings whose
148 /// `base()` is structure-relative (e.g. `InlineBacked<N>` returns
149 /// `&self.storage`) report a DIFFERENT address before and after the
150 /// backing has been moved. An absolute pointer captured at construction
151 /// time would then point at the backing's OLD location after `Self {
152 /// backing, ... }` moves the backing into Self by-value — silently
153 /// corrupting every subsequent `insert` / `get` / `remove`. Matches the
154 /// Slab move-safety pattern (see `UntypedSlab::base_offset` in size_classed.rs).
155 slots_offset: usize,
156 capacity: u32,
157 /// Free-list head: `None` = empty (must carve from `len`).
158 free_head: Option<u32>,
159 /// First slot never yet allocated.
160 len: u32,
161 backing_layout: NonZeroLayout,
162 /// Ties `T` and `G` to the struct so the field-less slot region (we
163 /// store only an offset, not a typed pointer) still carries the right
164 /// type identity. `*const` keeps `Send`/`Sync` opt-in via the explicit
165 /// `unsafe impl`s below.
166 _phantom: PhantomData<(*const T, G)>,
167}
168
169impl<T, B: Allocator + FixedRange> GenerationalSlab<T, B, u32> {
170 /// Construct with default `u32` generation counter.
171 pub fn new(capacity: usize, backing: B) -> Result<Self, AllocError> {
172 Self::with_generation(capacity, backing)
173 }
174}
175
176impl<T, B: Allocator + FixedRange, G: GenerationInt> GenerationalSlab<T, B, G> {
177 /// Construct with an explicit generation-width parameter.
178 ///
179 /// Use `u64` for high-churn long-running servers where 2^32 reuses of
180 /// the same slot could realistically occur.
181 pub fn with_generation(capacity: usize, backing: B) -> Result<Self, AllocError> {
182 if capacity == 0 {
183 return Err(AllocError);
184 }
185 let cap_u32 = u32::try_from(capacity).map_err(|_| AllocError)?;
186 let layout = NonZeroLayout::array::<GenerationalSlot<T, G>>(capacity).ok_or(AllocError)?;
187 let block = backing.allocate(layout)?;
188 let slots_ptr = block.cast::<GenerationalSlot<T, G>>();
189 // Compute offset from backing.base() — see field docs on
190 // `slots_offset`. Absolute `slots_ptr` is only valid in the local
191 // frame; after `Self { backing, ... }` moves `backing` by-value,
192 // any structure-relative backing's base address changes.
193 let base_addr = backing.base().as_ptr() as usize;
194 let slots_addr = slots_ptr.as_ptr() as usize;
195 debug_assert!(
196 slots_addr >= base_addr,
197 "GenerationalSlab: allocate() returned a pointer below backing.base() — \
198 impossible if `B: FixedRange` honours the OsBacked / FixedRange contract"
199 );
200 let slots_offset = slots_addr - base_addr;
201 // Initialize every slot to Free with generation ZERO.
202 // SAFETY: `slots_ptr..slots_ptr+capacity` is uninitialized memory of
203 // the right size and alignment per the layout request. We write
204 // through the LOCAL (pre-move) pointer; the post-move recompute
205 // (`backing.base() + slots_offset`) hits the SAME bytes inside the
206 // moved backing's storage, since the bytes are a sub-region of the
207 // backing and travel with it on the move.
208 unsafe {
209 for i in 0..capacity {
210 let p = slots_ptr.as_ptr().add(i);
211 p.write(GenerationalSlot {
212 generation: G::ZERO,
213 state: SlotState::Free { next_free: None },
214 });
215 }
216 }
217 Ok(Self {
218 backing,
219 slots_offset,
220 capacity: cap_u32,
221 free_head: None,
222 len: 0,
223 backing_layout: layout,
224 _phantom: PhantomData,
225 })
226 }
227
228 /// Recompute the live slot-array pointer from the backing's *current*
229 /// base. Must be called every time the slab needs to dereference into
230 /// the slot region — never cached across calls.
231 ///
232 /// For backings with a structure-relative `base()` (e.g.
233 /// `InlineBacked<N>`), the backing's storage moves with the
234 /// `GenerationalSlab` struct itself. The offset captured at
235 /// construction stays valid (the relative position of the slot array
236 /// inside the backing's storage is invariant); recomputing through
237 /// the live `backing.base()` plus that offset always lands on the
238 /// correct bytes.
239 #[inline]
240 fn slots(&self) -> NonNull<GenerationalSlot<T, G>> {
241 let base = self.backing.base().as_ptr();
242 // SAFETY: `slots_offset` was the difference between
243 // `backing.allocate(layout)`'s result and `backing.base()` at
244 // construction; the slot region is a sub-range of the backing's
245 // storage and `slots_offset + capacity * size_of` <= region.
246 let p = unsafe { base.add(self.slots_offset) };
247 // SAFETY: derived from a non-null backing.base() plus an in-range
248 // offset; the result is non-null.
249 unsafe { NonNull::new_unchecked(p.cast::<GenerationalSlot<T, G>>()) }
250 }
251
252 /// Number of slots.
253 #[inline]
254 pub fn capacity(&self) -> usize {
255 self.capacity as usize
256 }
257
258 /// Insert a value and return its handle. Errors if the slab is full.
259 pub fn insert(&mut self, value: T) -> Result<Handle<T, G>, AllocError> {
260 // Try free list first. Resolve `slot_idx` AND `slot` pointer in a
261 // single branch so the freelist-pop path does ONE
262 // `slots.as_ptr().add(idx)` computation rather than two — the
263 // optimizer should CSE, but folding the access ourselves avoids
264 // relying on noalias deduction across the `self.free_head =
265 // *next_free` write that sits between the two derefs.
266 // Defense-in-depth: `free_head` is only ever set from `remove` with an
267 // index in `0..len <= capacity`, so it cannot legitimately be out of
268 // range. Guard the one unchecked index path so a corrupted `free_head`
269 // can never cause an out-of-bounds slot deref (UB) — abandon the link
270 // and carve fresh instead. (Not reachable via the safe API today.)
271 if let Some(idx) = self.free_head {
272 if idx >= self.capacity {
273 debug_assert!(
274 false,
275 "GenerationalSlab: corrupt free_head {idx} >= capacity {}",
276 self.capacity,
277 );
278 self.free_head = None;
279 }
280 }
281 let (slot_idx, slot) = match self.free_head {
282 Some(idx) => {
283 // SAFETY: idx came from our free_head (set by remove), and the
284 // guard above ensured idx < capacity, so the slot is in bounds.
285 let slot = unsafe { &mut *self.slots().as_ptr().add(idx as usize) };
286 match &slot.state {
287 SlotState::Free { next_free } => {
288 self.free_head = *next_free;
289 }
290 SlotState::Occupied(_) => {
291 // Inconsistent state — the free list pointed at an
292 // occupied slot. Not reachable via the safe API; signal
293 // it in debug (mirroring the corrupt-`free_head` guard
294 // above) rather than silently masquerading as OOM.
295 debug_assert!(
296 false,
297 "GenerationalSlab: free_head {idx} points at an occupied slot",
298 );
299 return Err(AllocError);
300 }
301 }
302 (idx, slot)
303 }
304 None => {
305 if self.len >= self.capacity {
306 return Err(AllocError);
307 }
308 let idx = self.len;
309 self.len += 1;
310 // SAFETY: idx < capacity (we just checked); slot is
311 // initialized by `with_generation`.
312 let slot = unsafe { &mut *self.slots().as_ptr().add(idx as usize) };
313 (idx, slot)
314 }
315 };
316 // Place the value and capture the current generation for the handle.
317 slot.state = SlotState::Occupied(value);
318 let generation = slot.generation;
319 // Defense-in-depth: the slot we just filled must lie within
320 // `[0, len)` — freelist-pop returns previously-allocated slots
321 // and the fresh-allocation branch above bumped `self.len`. A
322 // mismatch indicates either freelist corruption or a stale
323 // `len` (refactor regression). Compiled out in release.
324 debug_assert!(
325 (slot_idx as usize) < self.len as usize,
326 "GenerationalSlab::insert: filled slot {slot_idx} but len={} — \
327 freelist corruption or stale len",
328 self.len,
329 );
330 Ok(Handle {
331 index: slot_idx,
332 generation,
333 _phantom: PhantomData,
334 })
335 }
336
337 /// Borrow the value behind a handle, or `None` if stale / vacant.
338 pub fn get(&self, handle: Handle<T, G>) -> Option<&T> {
339 if handle.index >= self.capacity {
340 return None;
341 }
342 // SAFETY: index is in-range; slot is initialized.
343 let slot = unsafe { &*self.slots().as_ptr().add(handle.index as usize) };
344 if slot.generation != handle.generation {
345 return None;
346 }
347 match &slot.state {
348 SlotState::Occupied(v) => Some(v),
349 SlotState::Free { .. } => None,
350 }
351 }
352
353 /// Borrow mutably behind a handle, or `None` if stale / vacant.
354 pub fn get_mut(&mut self, handle: Handle<T, G>) -> Option<&mut T> {
355 if handle.index >= self.capacity {
356 return None;
357 }
358 // SAFETY: as above; &mut self gives exclusive access.
359 let slot = unsafe { &mut *self.slots().as_ptr().add(handle.index as usize) };
360 if slot.generation != handle.generation {
361 return None;
362 }
363 match &mut slot.state {
364 SlotState::Occupied(v) => Some(v),
365 SlotState::Free { .. } => None,
366 }
367 }
368
369 /// Remove the value behind a handle and return it, or `None` if stale.
370 /// Bumps the slot's generation so the handle (and any copies) become
371 /// invalid.
372 pub fn remove(&mut self, handle: Handle<T, G>) -> Option<T> {
373 if handle.index >= self.capacity {
374 return None;
375 }
376 // SAFETY: as above.
377 let slot = unsafe { &mut *self.slots().as_ptr().add(handle.index as usize) };
378 if slot.generation != handle.generation {
379 return None;
380 }
381 // Bump the generation so the old handle is now stale. `wrapping_inc`
382 // reaches `ZERO` only from the type's max value, so `next == ZERO`
383 // means this slot just completed a full 2^bits cycle.
384 let next_gen = slot.generation.wrapping_inc();
385 slot.generation = next_gen;
386 let prior = if next_gen == G::ZERO {
387 // The generation wrapped. Returning the slot to the freelist now
388 // would let it be reused at generation 0 (and onward), which is
389 // exactly when a stale handle minted earlier in the cycle would
390 // re-validate against a different `T` — the ABA / UAF this type
391 // exists to prevent. RETIRE the slot instead: extract the value
392 // but leave it OFF the freelist (a tombstone), so it is never
393 // handed out again. Costs one slot per full wrap (4.3B removes for
394 // `u32`) — negligible — and closes the wraparound window entirely
395 // rather than merely documenting it.
396 core::mem::replace(&mut slot.state, SlotState::Free { next_free: None })
397 } else {
398 // Normal path: link the slot onto the freelist for reuse.
399 let prior = core::mem::replace(
400 &mut slot.state,
401 SlotState::Free {
402 next_free: self.free_head,
403 },
404 );
405 self.free_head = Some(handle.index);
406 prior
407 };
408 match prior {
409 SlotState::Occupied(v) => Some(v),
410 SlotState::Free { .. } => None, // can't happen; we checked above
411 }
412 }
413
414 /// True if the handle would currently access a live value.
415 #[inline]
416 pub fn contains(&self, handle: Handle<T, G>) -> bool {
417 self.get(handle).is_some()
418 }
419
420 /// Test-only: force a live slot's generation, so the wraparound-retirement
421 /// path can be reached without 2^bits real removes.
422 #[cfg(test)]
423 fn force_generation(&mut self, index: u32, generation: G) {
424 assert!(index < self.capacity);
425 // SAFETY: index < capacity; slot is initialized.
426 let slot = unsafe { &mut *self.slots().as_ptr().add(index as usize) };
427 slot.generation = generation;
428 }
429}
430
431impl<T, B: Allocator + FixedRange, G: GenerationInt> Drop for GenerationalSlab<T, B, G> {
432 fn drop(&mut self) {
433 // Drop every Occupied value, then return the backing chunk.
434 //
435 // **Panic-safety contract**: `T::drop` is allowed to panic. If it
436 // does:
437 //
438 // - During **normal** drop (no in-flight unwind): the panic
439 // escapes this body. Slots at indices > i are left undropped
440 // (Rust's loop unwind semantics — `mem::replace`'s temporary
441 // is the unwinding value), and the `backing.deallocate` call
442 // below this loop **does not run**, leaking the entire
443 // backing region. This is strictly worse than Slab's pattern
444 // (Slab deallocates the backing FIRST), but unlike Slab we
445 // cannot reorder — we need the backing memory live to read
446 // each `slot.state` pointer.
447 // - During **drop while unwinding** (a panic was already in
448 // flight when `GenerationalSlab` started dropping): a second
449 // panic in `T::drop` triggers **immediate process abort**
450 // per Rust language rules (`drop-while-panicking → abort`).
451 // This is unavoidable without `catch_unwind` (std-only) and
452 // matches the standard library's contract everywhere.
453 //
454 // Production guidance: ensure `T::drop` is panic-free for any
455 // `T` stored in a `GenerationalSlab`. Wrap fallible cleanup in
456 // a separate `try_close()`-style API and run it before the slab
457 // drops.
458 //
459 // SAFETY: every slot 0..capacity was initialized in construction.
460 // Use the live `self.slots()` accessor so the post-move backing
461 // base is honoured — caching the construction-time pointer would
462 // double-bug the structure-relative-backing case at drop time too.
463 unsafe {
464 let slots = self.slots();
465 for i in 0..self.capacity as usize {
466 let slot = &mut *slots.as_ptr().add(i);
467 if let SlotState::Occupied(_) = &slot.state {
468 // Replace with Free to run T's Drop.
469 let _ =
470 core::mem::replace(&mut slot.state, SlotState::Free { next_free: None });
471 }
472 }
473 self.backing
474 .deallocate(slots.cast::<u8>(), self.backing_layout);
475 }
476 }
477}
478
479// Send when T, B, G are Send. Sync when T, B are Sync; G is Copy and !Sync
480// is impossible for Copy types in practice. The slab itself permits &mut
481// access through methods, so cross-thread use via Arc<Mutex<...>> is the
482// standard pattern.
483unsafe impl<T: Send, B: Allocator + FixedRange + Send, G: GenerationInt + Send> Send
484 for GenerationalSlab<T, B, G>
485{
486}
487unsafe impl<T: Sync, B: Allocator + FixedRange + Sync, G: GenerationInt + Sync> Sync
488 for GenerationalSlab<T, B, G>
489{
490}
491
492// ============================================================================
493// Kani proof harnesses
494// ============================================================================
495
496#[cfg(kani)]
497mod kani_proofs {
498 use super::*;
499 use crate::backing::InlineBacked;
500
501 /// `insert` then `get` returns the inserted value via the issued handle.
502 #[kani::proof]
503 #[kani::unwind(5)]
504 fn insert_then_get_round_trips() {
505 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
506 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
507 let v: u64 = kani::any();
508 if let Ok(h) = s.insert(v) {
509 assert!(s.get(h) == Some(&v));
510 }
511 }
512
513 /// After `remove`, the old handle no longer resolves (`get` returns
514 /// `None`) — the generation counter on the slot mismatches the
515 /// handle's frozen generation.
516 #[kani::proof]
517 #[kani::unwind(5)]
518 fn remove_invalidates_old_handle() {
519 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
520 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
521 if let Ok(h) = s.insert(0xAA) {
522 let _ = s.remove(h);
523 // After remove, the handle's generation is stale.
524 assert!(s.get(h).is_none());
525 }
526 }
527
528 /// Reusing a slot via subsequent insert hands out a NEW handle whose
529 /// generation differs from any prior handle to the same slot.
530 #[kani::proof]
531 #[kani::unwind(5)]
532 fn reused_slot_gets_new_handle() {
533 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
534 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
535 if let Ok(h_old) = s.insert(0xAA) {
536 let _ = s.remove(h_old);
537 if let Ok(h_new) = s.insert(0xBB) {
538 // Same slot index could be reused, but generation must
539 // have moved — so the two handles are not interchangeable.
540 assert!(s.get(h_old).is_none());
541 assert!(s.get(h_new) == Some(&0xBB));
542 }
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::backing::InlineBacked;
551
552 #[test]
553 fn insert_then_get_round_trip() {
554 let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
555 GenerationalSlab::new(16, InlineBacked::<1024>::new()).unwrap();
556 let h = s.insert(42).unwrap();
557 assert_eq!(s.get(h), Some(&42));
558 }
559
560 /// A slot whose generation wraps a full cycle on `remove` must be RETIRED
561 /// (never returned to the freelist), closing the ABA/UAF window. With a
562 /// capacity-2 slab: slot 0 is forced to `u32::MAX`, removed (→ wraps →
563 /// retired), then the slab can only ever hand out slot 1 — a third insert
564 /// must OOM rather than reuse the retired slot 0.
565 #[test]
566 fn wrapped_generation_slot_is_retired() {
567 let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
568 GenerationalSlab::with_generation(2, InlineBacked::<1024>::new()).unwrap();
569 // Force slot 0 (carved by the first insert) to MAX *before* inserting,
570 // so the handle is minted at MAX and the matching `remove` wraps to 0.
571 s.force_generation(0, u32::MAX);
572 let a = s.insert(1).unwrap();
573 assert_eq!(a.index, 0);
574 assert_eq!(a.generation, u32::MAX);
575 assert_eq!(s.remove(a), Some(1)); // gen matches → wraps → retires slot 0
576 // Slot 0 retired. The fresh carve gives slot 1; a third insert OOMs
577 // because slot 0 is off the freelist (without retirement it would be
578 // reused and `c` below would succeed at index 0).
579 let b = s.insert(2).unwrap();
580 assert_eq!(b.index, 1, "retired slot 0 must not be reused");
581 assert!(
582 s.insert(3).is_err(),
583 "retired slot must stay out of circulation (no reuse)",
584 );
585 }
586
587 #[test]
588 fn remove_invalidates_handle() {
589 let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
590 GenerationalSlab::new(16, InlineBacked::<1024>::new()).unwrap();
591 let h = s.insert(42).unwrap();
592 assert_eq!(s.remove(h), Some(42));
593 // Second access through the same handle must return None.
594 assert_eq!(s.get(h), None);
595 assert_eq!(s.remove(h), None);
596 }
597
598 #[test]
599 fn reused_slot_returns_new_value_only_to_new_handle() {
600 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
601 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
602 let h1 = s.insert(1).unwrap();
603 s.remove(h1);
604 let h2 = s.insert(2).unwrap();
605 // ABA test: stale h1 must NOT see h2's value.
606 assert_eq!(s.get(h1), None, "stale handle must not access reused slot");
607 assert_eq!(s.get(h2), Some(&2));
608 }
609
610 #[test]
611 fn capacity_exhaustion_returns_err() {
612 let mut s: GenerationalSlab<u64, InlineBacked<256>> =
613 GenerationalSlab::new(4, InlineBacked::<256>::new()).unwrap();
614 for i in 0..4 {
615 s.insert(i).unwrap();
616 }
617 assert!(s.insert(5).is_err());
618 }
619
620 #[test]
621 fn get_mut_mutates() {
622 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
623 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
624 let h = s.insert(0).unwrap();
625 *s.get_mut(h).unwrap() = 99;
626 assert_eq!(s.get(h), Some(&99));
627 }
628
629 #[test]
630 fn handles_are_copy() {
631 let mut s: GenerationalSlab<u64, InlineBacked<256>> =
632 GenerationalSlab::new(2, InlineBacked::<256>::new()).unwrap();
633 let h = s.insert(7).unwrap();
634 let h2 = h; // Copy
635 assert_eq!(s.get(h), Some(&7));
636 assert_eq!(s.get(h2), Some(&7));
637 }
638
639 #[test]
640 #[cfg(feature = "std")]
641 fn drops_occupied_values() {
642 use std::sync::atomic::{AtomicUsize, Ordering};
643 use std::sync::Arc;
644 struct Counter(Arc<AtomicUsize>);
645 impl Drop for Counter {
646 fn drop(&mut self) {
647 self.0.fetch_add(1, Ordering::Relaxed);
648 }
649 }
650 let drops = Arc::new(AtomicUsize::new(0));
651 {
652 let mut s: GenerationalSlab<Counter, InlineBacked<512>> =
653 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
654 s.insert(Counter(drops.clone())).unwrap();
655 s.insert(Counter(drops.clone())).unwrap();
656 // slab dropped here; both Counters should run Drop.
657 }
658 assert_eq!(drops.load(Ordering::Relaxed), 2);
659 }
660
661 /// Regression: `GenerationalSlab` used to
662 /// cache `slots: NonNull<GenerationalSlot<T, G>>` captured from
663 /// `backing.allocate(...)` BEFORE the backing was moved into Self. For
664 /// structure-relative backings like `InlineBacked<N>` (whose `base()`
665 /// returns `&self.storage`), the cached pointer aimed at the *pre-move*
666 /// location forever, silently corrupting every subsequent
667 /// `insert`/`get`/`remove`.
668 ///
669 /// This test constructs a slab inside a helper, returns it by value to
670 /// a caller frame, defeats NRVO with a large stack local, then exercises
671 /// the slab. Each access must land on the LIVE backing's slot region,
672 /// not the pre-move location.
673 #[test]
674 fn slab_survives_move_with_inline_backed_nrvo_defeating() {
675 #[inline(never)]
676 fn make() -> GenerationalSlab<u64, InlineBacked<512>> {
677 let mut s: GenerationalSlab<u64, InlineBacked<512>> =
678 GenerationalSlab::new(8, InlineBacked::<512>::new()).unwrap();
679 // Pre-populate from the constructor frame so the bug's window
680 // is exercised: writes here go through the construction-time
681 // backing location. The returned struct must still read the
682 // SAME values from the moved backing's storage.
683 for i in 0..4u64 {
684 let _ = s.insert(0xAA00 + i).unwrap();
685 }
686 s
687 }
688 // Force enough locals to defeat NRVO. The 4096-byte array sits
689 // on the caller's stack, pushing the returned `slab` to a
690 // different address than `make`'s local.
691 let _arr = [0u8; 4096];
692 let mut slab = make();
693 // The construction-time pointer (if cached) would now be stale.
694 let h_new = slab.insert(0xDEADBEEF).unwrap();
695 // Live read through the new handle — must round-trip.
696 assert_eq!(slab.get(h_new), Some(&0xDEADBEEF));
697 // Insert/get round trips for more values to stress the post-move
698 // slot region.
699 let mut handles = alloc::vec::Vec::new();
700 for v in 0..3u64 {
701 handles.push(slab.insert(0xCAFE_0000 + v).unwrap());
702 }
703 for (i, h) in handles.iter().enumerate() {
704 assert_eq!(slab.get(*h), Some(&(0xCAFE_0000 + i as u64)));
705 }
706 // Remove + insert: exercise the freelist path on the post-move
707 // storage. If the slots pointer were stale, the bumped generation
708 // would be written into the WRONG slot and the new handle's read
709 // would either return None or the stale value.
710 assert_eq!(slab.remove(h_new), Some(0xDEADBEEF));
711 let h_reuse = slab.insert(0x1234).unwrap();
712 assert_eq!(slab.get(h_reuse), Some(&0x1234));
713 }
714
715 /// Companion: assert that the slot region the slab dereferences
716 /// actually lies inside the LIVE backing's range after the move.
717 /// A stale-pointer bug would have the slab reading from a different
718 /// address range than `slab.backing.base()` reports.
719 #[test]
720 fn slab_slots_lie_inside_live_backing_after_move() {
721 #[inline(never)]
722 fn make() -> GenerationalSlab<u64, InlineBacked<512>> {
723 GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap()
724 }
725 let _arr = [0u8; 4096];
726 let mut slab = make();
727 // Insert and capture the address of the slot.
728 let h = slab.insert(0x55AA).unwrap();
729 let val_ref: &u64 = slab.get(h).unwrap();
730 let val_addr = val_ref as *const u64 as usize;
731 // Resolve the live backing base + size via FixedRange.
732 let base = FixedRange::base(&slab.backing).as_ptr() as usize;
733 let size = FixedRange::size(&slab.backing);
734 assert!(
735 val_addr >= base && val_addr < base + size,
736 "slot address {val_addr:#x} outside live backing range \
737 [{base:#x}, {:#x}) — stale-pointer bug",
738 base + size,
739 );
740 }
741}