Skip to main content

forge_alloc/layout/
slab.rs

1//! `Slab<T, B, M>` — typed fixed-stride block allocator with optional
2//! freelist MAC.
3//!
4//! Each slot holds either a live `T` (size & alignment per `T`) or a
5//! a `FreeLink` (when on the free list). The block stride is the max of the
6//! two, so slot reuse is always size- and alignment-compatible.
7//!
8//! Freelist uses 1-based indices (`0` = empty), separating the "list empty"
9//! sentinel from any valid slot — see `docs/ARCHITECTURE.md` for the
10//! freelist design.
11
12use core::cell::UnsafeCell;
13use core::marker::PhantomData;
14use core::mem::{align_of, size_of};
15use core::ptr::NonNull;
16use core::sync::atomic::{AtomicUsize, Ordering};
17
18use forge_alloc_core::{
19    AllocError, Allocator, Deallocator, FixedRange, FreelistProtection, NoProtection, NonZeroLayout,
20};
21
22// Cross-check: `B: FixedRange` is required so that `Slab` can re-query
23// `self.backing.base()` after the struct has moved. Backings whose
24// `base()` is structure-relative (e.g. `InlineBacked`) return DIFFERENT
25// addresses before and after a move; storing an absolute `NonNull<u8>`
26// captured at construction time would leave the slab pointing at the
27// OLD location, corrupting every subsequent `allocate` / `deallocate`.
28// See the "Slab move-safety" pin test for the exact failure mode.
29
30/// A free-list link stored inside a free slot.
31///
32/// `next_idx` is the 1-based index of the next free slot; `0` means end of
33/// list. `mac` is the integrity tag computed by [`FreelistProtection::sign`];
34/// [`NoProtection`] always writes `0`. The field exists unconditionally so
35/// that block_stride is stable across `M` choices.
36#[repr(C)]
37#[derive(Copy, Clone)]
38struct FreeLink {
39    next_idx: u32,
40    mac: u32,
41}
42
43/// Fixed-stride typed slab.
44///
45/// `T` is the value type; `B` is the underlying backing (any `Allocator`);
46/// `M` is the freelist integrity policy (default [`NoProtection`], with
47/// zero overhead). The slab takes one large allocation from `B` at
48/// construction; individual `allocate` / `deallocate` calls return slots
49/// from within that allocation in O(1).
50///
51/// # Usage discipline (read before unsafe-calling either trait method)
52///
53/// `Slab` issues raw `NonNull<u8>` pointers into typed-but-uninitialised
54/// slots — it does **not** track value lifecycle. The caller is responsible
55/// for the four following invariants:
56///
57/// 1. **Allocate-then-write**: the bytes inside the returned slice are
58///    uninitialised. The caller must write a valid `T` (e.g. via
59///    `core::ptr::write`) before reading.
60/// 2. **Drop-before-deallocate**: `deallocate` overwrites the slot with a
61///    a `FreeLink`. If `T: Drop`, the caller must run `T`'s destructor
62///    (typically `core::ptr::drop_in_place::<T>(ptr.as_ptr().cast())`)
63///    *before* calling `deallocate`. Failure to do so leaks resources owned
64///    by `T` (file handles, heap allocations, locks) and may cause UB if
65///    `T`'s drop is required for soundness.
66/// 3. **Layout-must-fit-stride**: callers should request layouts whose
67///    `size <= block_stride()` and `align <= max(align_of::<T>(),
68///    align_of::<FreeLink>())`. Mis-sized requests fail at allocate;
69///    mis-aligned ones may not match a slot index and trip a debug
70///    assertion in `deallocate`.
71/// 4. **Slab-drop-does-not-drop-Ts**: when the `Slab` itself drops, it
72///    returns the underlying backing region to `B` but does *not* iterate
73///    live slots to drop `T`. Callers responsible for any still-live `T`
74///    must drain them before dropping the slab — e.g. via a higher-level
75///    typed wrapper such as `GenerationalSlab`, which tracks per-slot
76///    generations and runs `T::drop` for outstanding handles.
77///
78/// `Slab::allocate` returns a `NonNull<[u8]>` whose slice length is
79/// [`block_stride()`](Self::block_stride), **not** the requested
80/// `layout.size()`. Callers who care about the exact requested size must
81/// remember it themselves; callers who want to use the extra padding bytes
82/// (e.g. for footers / metadata) may write through the full stride window.
83///
84/// # Thread safety
85///
86/// `Send` if `T`, `B`, `M` are `Send`. `Sync`: NO. The free list head and
87/// next-uncarved cursor live in `UnsafeCell`s so that `Allocator::allocate`
88/// can take `&self`. Cross-thread deallocation uses `SlabRemote` —
89/// not this type directly.
90///
91/// # API-misuse compile-failures (pinned)
92///
93/// `T` must not be a zero-sized type. The `ASSERT_T_NON_ZST` associated
94/// const turns the previously runtime-only rejection
95/// (`size_of::<T>() == 0` → `AllocError`) into a build error, so the
96/// failure surfaces at the call site instead of after a successful build.
97///
98/// ```compile_fail
99/// // FAILS TO COMPILE: ZST T is rejected by `Slab::ASSERT_T_NON_ZST`.
100/// // The const_assert fires when `with_protection` is monomorphised, so
101/// // the build halts before any test runs.
102/// use forge_alloc::InlineBacked;
103/// use forge_alloc::Slab;
104/// let _: Slab<(), InlineBacked<128>> =
105///     Slab::new(8, InlineBacked::<128>::new()).unwrap();
106/// ```
107pub struct Slab<T, B: Allocator + FixedRange, M: FreelistProtection = NoProtection> {
108    backing: B,
109    mac: M,
110    /// Byte offset from `backing.base()` to the start of the slab's
111    /// `capacity * block_stride` slot region.
112    ///
113    /// We deliberately do NOT store an absolute pointer. Backings whose
114    /// `base()` is structure-relative (e.g. `InlineBacked<N>` returns
115    /// `&self.storage`) report a DIFFERENT address before and after the
116    /// backing has been moved. An absolute pointer captured at
117    /// construction would then point at the backing's OLD location after
118    /// the slab was returned from its constructor by value. Storing the
119    /// offset and computing `self.backing.base().as_ptr().add(offset)`
120    /// at each access keeps the slab move-safe.
121    base_offset: usize,
122    /// 1-based slot index; 0 = list empty.
123    free_head: UnsafeCell<u32>,
124    /// Index of the first slot never yet allocated (always carved from front).
125    next_uncarved: UnsafeCell<u32>,
126    block_stride: usize,
127    /// `block_stride.trailing_zeros()` when `block_stride.is_power_of_two()`,
128    /// `0` otherwise. `slot_index` uses this to replace runtime `/ stride`
129    /// and `% stride` with `>> shift` and `& (stride - 1)` for the common
130    /// pow2-stride case (every `T` whose size and align are powers of two
131    /// with size ≥ `size_of::<FreeLink>()`, which is most real types). The
132    /// sentinel `0` is safe because real strides are always ≥ `size_of::<FreeLink>()` = 8,
133    /// so a true pow2 stride has shift ≥ 3.
134    stride_shift: u32,
135    capacity: u32,
136    backing_layout: NonZeroLayout,
137    /// Count of detected freelist corruption events (MAC verify
138    /// failures + out-of-range `next_idx` defense-in-depth tripwires).
139    /// Each event bumps this counter before the freelist is abandoned;
140    /// the slab keeps serving allocations from `next_uncarved`. Exposed
141    /// via [`Allocator::corruption_events`] for operator observability.
142    ///
143    /// **Width:** `AtomicUsize` (not `AtomicU64`) so this compiles on
144    /// 32-bit bare-metal targets (Cortex-M3/M4, `thumbv7em-none-eabihf`)
145    /// that lack native 64-bit atomic ops. The trait method
146    /// [`Allocator::corruption_events`] still returns `u64`; the cast
147    /// happens at the trait boundary. Practical impact: on 32-bit
148    /// hosts the counter saturates at `u32::MAX ≈ 4.3 B` corruption
149    /// events — overflow is irrelevant in any realistic timeframe
150    /// (one event/ns ≈ 4.3 s, but real workloads see ≪1 event/year).
151    corruption_events: AtomicUsize,
152    _phantom: PhantomData<T>,
153}
154
155impl<T, B: Allocator + FixedRange> Slab<T, B, NoProtection> {
156    /// Construct a slab with the default `NoProtection` policy.
157    ///
158    /// `capacity` is the number of `T` slots. Errors if the backing cannot
159    /// supply the required region or if the total size overflows.
160    pub fn new(capacity: usize, backing: B) -> Result<Self, AllocError> {
161        Self::with_protection(capacity, backing, NoProtection)
162    }
163}
164
165impl<T, B: Allocator + FixedRange, M: FreelistProtection> Slab<T, B, M> {
166    /// Compile-time assertion that `T` is not a ZST.
167    ///
168    /// Forcing this associated const inside `with_protection` triggers a
169    /// compile error when the slab is instantiated with a ZST `T`,
170    /// promoting the previously runtime-only ZST rejection (`size_of::<T>()
171    /// == 0` → `AllocError`) to a build-time error. This is purely
172    /// additive: every `T` that was accepted before still compiles, and
173    /// the runtime check below remains for backwards-compatibility and
174    /// defense-in-depth against any future generic path that might bypass
175    /// the const.
176    const ASSERT_T_NON_ZST: () = assert!(
177        size_of::<T>() > 0,
178        "Slab<T, B, M>: T must not be a zero-sized type — a freelist over \
179         zero-byte slots has no meaningful pointer arithmetic. See the \
180         `compile_fail` doctest on `Slab` for the rejection example.",
181    );
182
183    /// Construct a slab with an explicit freelist-protection policy.
184    ///
185    /// `capacity` must be `> 0` and ≤ `u32::MAX` — the slab uses 32-bit slot
186    /// indices internally. `T` must not be a ZST (a freelist over zero-sized
187    /// slots has no meaning); this is now enforced at **compile time** via
188    /// `ASSERT_T_NON_ZST` — instantiating `Slab<(), _, _>` is a build error
189    /// rather than a runtime `AllocError`.
190    pub fn with_protection(capacity: usize, backing: B, mac: M) -> Result<Self, AllocError> {
191        // Force compile-time evaluation of the ZST check. If `T` is a ZST
192        // the build fails here; otherwise the const is `()` and emits no
193        // code.
194        let _: () = Self::ASSERT_T_NON_ZST;
195        // capacity == 0 makes the slab unusable.
196        if capacity == 0 {
197            return Err(AllocError);
198        }
199        // ZST T: belt-and-braces — `ASSERT_T_NON_ZST` already rejected
200        // this at compile time, but a future generic path that somehow
201        // bypasses the const should still produce an honest error rather
202        // than dividing by zero downstream.
203        if size_of::<T>() == 0 {
204            return Err(AllocError);
205        }
206        // Slot indices fit in `u32`; reject overly large slabs up front.
207        let cap_u32 = u32::try_from(capacity).map_err(|_| AllocError)?;
208
209        // block_stride = max(size_of::<T>(), size_of::<FreeLink>()), then
210        // round up to max(align_of::<T>(), align_of::<FreeLink>()) so each
211        // slot is properly aligned for both views.
212        let slot_align = core::cmp::max(align_of::<T>(), align_of::<FreeLink>());
213        let raw_stride = core::cmp::max(size_of::<T>(), size_of::<FreeLink>());
214        // Round `raw_stride` up to `slot_align` without risking overflow on
215        // pathologically large `T`. `slot_align` is always a power of two so
216        // the mask is correct.
217        let block_stride = raw_stride
218            .checked_add(slot_align - 1)
219            .map(|v| v & !(slot_align - 1))
220            .ok_or(AllocError)?;
221
222        // Total bytes = capacity * block_stride.
223        let total = block_stride.checked_mul(capacity).ok_or(AllocError)?;
224        let backing_layout =
225            NonZeroLayout::from_size_align(total, slot_align).map_err(|_| AllocError)?;
226
227        let block = backing.allocate(backing_layout)?;
228        // Capture the OFFSET of the allocated region from `backing.base()`.
229        // We need a stable identifier that survives the imminent move of
230        // `backing` into `Self`; the offset is invariant under struct
231        // moves (the relative layout inside the backing is fixed), while
232        // an absolute `NonNull<u8>` captured here would point at the
233        // backing's pre-move address and silently corrupt every later
234        // access. See the struct-field comment on `base_offset`.
235        let block_addr = block.cast::<u8>().as_ptr() as usize;
236        let backing_base_addr = backing.base().as_ptr() as usize;
237        // `block_addr >= backing_base_addr` always for a fresh
238        // `backing.allocate(...)` whose backing implements `FixedRange`
239        // honestly. If the backing returns a pointer outside its own
240        // range, that's a backing bug, not ours — defend with a
241        // checked subtraction so we surface it as `AllocError` rather
242        // than producing a wrap-bounded offset that explodes later.
243        let base_offset = block_addr
244            .checked_sub(backing_base_addr)
245            .ok_or(AllocError)?;
246
247        // Pre-compute the pow2-stride shift; 0 is the "not pow2" sentinel
248        // (real strides are always ≥ 8, so pow2 strides have shift ≥ 3).
249        let stride_shift = if block_stride.is_power_of_two() {
250            block_stride.trailing_zeros()
251        } else {
252            0
253        };
254
255        Ok(Self {
256            backing,
257            mac,
258            base_offset,
259            free_head: UnsafeCell::new(0),
260            next_uncarved: UnsafeCell::new(0),
261            block_stride,
262            stride_shift,
263            capacity: cap_u32,
264            backing_layout,
265            corruption_events: AtomicUsize::new(0),
266            _phantom: PhantomData,
267        })
268    }
269
270    /// Resolve the slab's base pointer from the (current) backing
271    /// location plus the captured offset. Recomputing every call keeps
272    /// us safe against moves of the slab between construction and use.
273    #[inline]
274    fn base_ptr(&self) -> NonNull<u8> {
275        // SAFETY: `backing.base()` is the start of the backing's region;
276        // `base_offset` is `<= backing.size() - capacity*block_stride`
277        // (the backing.allocate at construction reserved that range).
278        // The resulting pointer is non-null because backing.base() is non-null.
279        unsafe { NonNull::new_unchecked(self.backing.base().as_ptr().add(self.base_offset)) }
280    }
281
282    /// Number of slots in this slab.
283    #[inline]
284    pub fn capacity(&self) -> usize {
285        self.capacity as usize
286    }
287
288    /// Bytes per slot (≥ `size_of::<T>()`).
289    #[inline]
290    pub fn block_stride(&self) -> usize {
291        self.block_stride
292    }
293
294    /// Borrow the underlying backing.
295    #[inline]
296    pub fn backing(&self) -> &B {
297        &self.backing
298    }
299
300    /// Pointer to slot `idx` (0-based). No bounds check — internal helper.
301    #[inline]
302    fn slot_ptr(&self, idx: u32) -> *mut u8 {
303        // SAFETY: base + idx*stride is in-range when idx < capacity. Callers
304        // verify the bound.
305        unsafe {
306            self.base_ptr()
307                .as_ptr()
308                .add(idx as usize * self.block_stride)
309        }
310    }
311
312    /// 0-based slot index for `ptr`, or `None` if it's not aligned to a slot
313    /// boundary or out of range.
314    #[inline]
315    fn slot_index(&self, ptr: NonNull<u8>) -> Option<u32> {
316        let p = ptr.as_ptr() as usize;
317        let base = self.base_ptr().as_ptr() as usize;
318        if p < base {
319            return None;
320        }
321        let offset = p - base;
322        // Pow2-stride fast path: replace `/ stride` and `% stride` with
323        // `>> shift` and `& (stride - 1)`. On x86-64 this removes a 20-40
324        // cycle integer divide from every deallocate when T's stride is a
325        // power of two (the common case: any T whose `size` and `align`
326        // are both powers of two and `size >= 8`).
327        let (idx, rem) = if self.stride_shift != 0 {
328            let mask = self.block_stride - 1;
329            (offset >> self.stride_shift, offset & mask)
330        } else {
331            (offset / self.block_stride, offset % self.block_stride)
332        };
333        if rem != 0 {
334            return None;
335        }
336        if idx >= self.capacity as usize {
337            return None;
338        }
339        // `idx < self.capacity` and `self.capacity: u32`, so `idx` always
340        // fits in `u32`. Use `try_from` to make that explicit and defend
341        // against future capacity-type changes.
342        u32::try_from(idx).ok()
343    }
344}
345
346unsafe impl<T, B: Allocator + FixedRange, M: FreelistProtection> Deallocator for Slab<T, B, M> {
347    /// Push the slot identified by `ptr` onto the freelist.
348    ///
349    /// # Safety
350    ///
351    /// Per the [`Deallocator`] contract, `ptr` must have been returned by a
352    /// previous call to `self.allocate(layout)`. Specifically:
353    ///
354    /// - `ptr` must lie at the base of a slot in this slab (not an offset
355    ///   within a slot, not a pointer from another slab or allocator).
356    /// - The caller is responsible for running `T`'s destructor (e.g. via
357    ///   `core::ptr::drop_in_place`) before calling `deallocate`. This method
358    ///   overwrites the slot's bytes with a `FreeLink`.
359    /// - Passing the same `ptr` twice without an intervening `allocate` is a
360    ///   double-free and is UB. **No protection level — including
361    ///   `SipHashMAC` — detects a base-of-slot double-free.** The tripwire
362    ///   (`next_idx <= capacity`) does not catch it: the second free rewrites
363    ///   the slot's own `FreeLink` to point at the still-live head (an in-range
364    ///   index), so later allocations alias the same live slot. `SipHashMAC`
365    ///   does not catch it either, because the MAC binds a link to *its own
366    ///   slot index*: the second free re-signs a perfectly valid MAC for that
367    ///   same index in place, which then verifies on pop. The MAC's protection
368    ///   is against a link *forged or relocated to a different slot* (and the
369    ///   move-safety false-fail the index nonce fixes) — not an in-place
370    ///   re-sign. *Detecting* double-free needs orthogonal per-slot state — a
371    ///   live/free bit or a generation tag, e.g.
372    ///   [`GenerationalSlab`](crate::GenerationalSlab), which rejects a stale
373    ///   handle on its second free — which this slab does not carry by design.
374    ///   A [`Quarantine`](crate::Quarantine) layer does *not* detect it (it
375    ///   keeps no per-slot state and would forward both frees to the inner
376    ///   slab); it only delays slot reuse, shrinking the window in which the
377    ///   aliased slot is handed back out.
378    #[inline]
379    unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: NonZeroLayout) {
380        // Layout sanity: an honest caller's layout fits within block_stride.
381        // Release builds skip the check (per contract this is UB anyway).
382        debug_assert!(
383            _layout.size().get() <= self.block_stride
384                && _layout.align().get() <= align_of::<T>().max(align_of::<FreeLink>()),
385            "Slab::deallocate: layout exceeds slot stride or alignment",
386        );
387
388        // Resolve the slot index. In a properly used Slab the index is valid;
389        // an out-of-range pointer is UB (per the trait contract). Debug
390        // builds catch it; release builds skip.
391        let idx = match self.slot_index(ptr) {
392            Some(i) => i,
393            None => {
394                debug_assert!(false, "Slab::deallocate: pointer outside slab range");
395                return;
396            }
397        };
398
399        // Push slot onto the free list.
400        // SAFETY: !Sync — no concurrent access to free_head.
401        unsafe {
402            let head_ptr = self.free_head.get();
403            let old_head = *head_ptr; // 1-based; 0 means empty
404                                      // Bind the MAC to the slot INDEX, not its absolute address. The
405                                      // index is move-invariant; the address is not — `InlineBacked` and
406                                      // other move-relative backings change every slot's address when the
407                                      // `Slab` is moved (the very reason `slot_ptr` re-derives through
408                                      // `&self`). Signing over `ptr.as_ptr()` here but verifying over the
409                                      // re-derived `slot_ptr(idx)` in `allocate` would spuriously fail for
410                                      // any slab moved between a free and a later alloc. The index is an
411                                      // equally strong nonce: it uniquely identifies the slot, so a link
412                                      // copied to a different slot still fails verification.
413            let mac = self.mac.sign(old_head, idx as usize);
414            let link = FreeLink {
415                next_idx: old_head,
416                mac,
417            };
418            // Write the FreeLink into the slot's memory.
419            //
420            // Stacked Borrows: we MUST NOT write through the user-supplied
421            // `ptr` directly. `ptr`'s provenance was derived from the
422            // backing's `SharedReadWrite` tag at allocate time, and an
423            // outer wrapper (e.g. `Quarantine::drop`, `SlabOwner::drop`,
424            // `PoisonOnFree::drop`) may have taken a `&mut self` covering
425            // the whole composition — that Unique retag invalidates the
426            // older SharedReadWrite tag in the borrow stack. Writing
427            // through the stale tag is then UB.
428            //
429            // The fix is to re-derive the slot pointer through `&self`:
430            // `self.slot_ptr(idx)` calls `self.base_ptr()` which calls
431            // `self.backing.base()`, each of which traverses fresh shared
432            // reborrows. The resulting pointer sits at the top of the
433            // borrow stack and is valid even after the outer Unique
434            // retag. (Miri caught the original bug across
435            // SlabOwner / Quarantine / PoisonOnFree / etc.)
436            let slot_ptr = self.slot_ptr(idx);
437            slot_ptr.cast::<FreeLink>().write(link);
438            *head_ptr = idx + 1; // store 1-based
439        }
440    }
441}
442
443unsafe impl<T, B: Allocator + FixedRange, M: FreelistProtection> Allocator for Slab<T, B, M> {
444    #[inline]
445    fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
446        // Reject layouts the slab cannot satisfy.
447        let req_align = layout.align().get();
448        let req_size = layout.size().get();
449        if req_align > align_of::<T>().max(align_of::<FreeLink>()) {
450            return Err(AllocError);
451        }
452        if req_size > self.block_stride {
453            return Err(AllocError);
454        }
455
456        // SAFETY: !Sync — no concurrent access to free_head or next_uncarved.
457        unsafe {
458            // Try to pop from the free list first.
459            let head_ptr = self.free_head.get();
460            let head = *head_ptr; // 1-based; 0 = empty
461            if head != 0 {
462                // head - 1 is the slot index.
463                let slot_idx = head - 1;
464                let slot = self.slot_ptr(slot_idx);
465                let link = slot.cast::<FreeLink>().read();
466                // Verify MAC. On corruption, drop the link (don't propagate)
467                // and fall through to next_uncarved — defense-in-depth, the
468                // attacker's poisoned link is now disarmed. The nonce is the
469                // slot INDEX (move-invariant), matching `deallocate`'s `sign`.
470                let mac_ok = self
471                    .mac
472                    .verify(link.next_idx, link.mac, slot_idx as usize)
473                    .is_ok();
474                // Defense-in-depth: even with `NoProtection` (or a future MAC
475                // impl with a bug), reject a next_idx that would cause OOB
476                // slot_ptr arithmetic. A valid free-list entry can only point
477                // to a slot index in `0..capacity` (we store `idx+1` as 1-based
478                // and the slab is single-threaded). `next_idx > capacity` ⇒
479                // either corruption or an out-of-spec foreign write — treat
480                // the same as MAC failure.
481                if mac_ok && link.next_idx <= self.capacity {
482                    *head_ptr = link.next_idx;
483                    return Ok(NonNull::slice_from_raw_parts(
484                        NonNull::new_unchecked(slot),
485                        self.block_stride,
486                    ));
487                } else {
488                    // Record the corruption event BEFORE the debug_assert
489                    // so the counter reflects the detection regardless of
490                    // build profile: in release the assert is compiled out
491                    // and only the counter remains; in debug the counter
492                    // is updated and then the assert panics — but the
493                    // counter increment is already visible to any panic
494                    // handler (or `catch_unwind`-wrapped test) that
495                    // inspects `corruption_events` post-detection. The
496                    // event is the FIRST observable sign of an in-progress
497                    // attack — silent disarm without the counter leaves
498                    // operators blind. Ordering of the two statements
499                    // matches `ExtendableSlab::deallocate` and
500                    // `UntypedSlab::{allocate_slot, free_slot}` so the
501                    // debug/release semantics agree across all
502                    // corruption-detect sites.
503                    //
504                    // `Relaxed` is correct: the counter is advisory,
505                    // monotonically increasing, and read eventually-
506                    // consistently — no other state synchronizes against
507                    // it.
508                    self.corruption_events.fetch_add(1, Ordering::Relaxed);
509                    debug_assert!(
510                        false,
511                        "Slab freelist corruption: mac_ok={mac_ok}, next_idx={}, capacity={}",
512                        link.next_idx, self.capacity,
513                    );
514                    // Abandon the free list to prevent following corrupted
515                    // links; force fresh allocation from next_uncarved.
516                    *head_ptr = 0;
517                }
518            }
519            // Carve from next_uncarved.
520            let nxt_ptr = self.next_uncarved.get();
521            let nxt = *nxt_ptr;
522            if nxt >= self.capacity {
523                return Err(AllocError);
524            }
525            let slot = self.slot_ptr(nxt);
526            *nxt_ptr = nxt + 1;
527            Ok(NonNull::slice_from_raw_parts(
528                NonNull::new_unchecked(slot),
529                self.block_stride,
530            ))
531        }
532    }
533
534    #[inline]
535    unsafe fn usable_size(&self, ptr: NonNull<u8>, _layout: NonZeroLayout) -> Option<usize> {
536        // Every slot is `block_stride` bytes, which can exceed the requested
537        // `layout.size()` (e.g. `Slab<u8>` → 8-byte slots). Report the true
538        // usable extent so an outer scrub wrapper (`PoisonOnFree`/
539        // `ZeroizeOnFree`) wipes the WHOLE slot on free, not just the requested
540        // prefix — otherwise the stride-slack tail keeps freed secret bytes.
541        // Contract `n >= layout.size()` holds: `allocate` rejects
542        // `req_size > block_stride`.
543        debug_assert!(
544            self.slot_index(ptr).is_some(),
545            "Slab::usable_size: pointer outside slab range",
546        );
547        Some(self.block_stride)
548    }
549
550    #[inline]
551    fn capacity_bytes(&self) -> Option<usize> {
552        Some(self.capacity as usize * self.block_stride)
553    }
554
555    #[inline]
556    fn corruption_events(&self) -> u64 {
557        // Cast `usize → u64` at the trait boundary so the public API
558        // stays uniform across 32-bit and 64-bit targets. Lossless: on
559        // 32-bit hosts the inner counter is u32 (≤ u32::MAX ≈ 4.3 B);
560        // on 64-bit hosts it is u64.
561        self.corruption_events.load(Ordering::Relaxed) as u64
562    }
563}
564
565impl<T, B: Allocator + FixedRange, M: FreelistProtection> FixedRange for Slab<T, B, M> {
566    #[inline]
567    fn base(&self) -> NonNull<u8> {
568        self.base_ptr()
569    }
570
571    #[inline]
572    fn size(&self) -> usize {
573        self.capacity as usize * self.block_stride
574    }
575}
576
577impl<T, B: Allocator + FixedRange, M: FreelistProtection> Drop for Slab<T, B, M> {
578    fn drop(&mut self) {
579        // Debug-only sanity check: walk the freelist and verify every
580        // carved slot has been returned. A mismatch means the caller
581        // dropped the slab with live allocations outstanding — for
582        // `T: Drop` the destructor never runs, which is a real leak of
583        // resources owned by `T` (heap allocations inside `T`, file
584        // handles, locks, etc.). For `T: Copy` (or any `!Drop` type),
585        // the only loss is the un-reclaimed slot index, which is fine
586        // because the backing region drops on the next line anyway.
587        // We therefore skip the check when `T: !Drop` so existing test
588        // patterns (allocate-then-drop-slab on `u64`-style payloads)
589        // continue to compile cleanly.
590        //
591        // Walks freelist links by `next_idx` only (no MAC verification);
592        // a corrupted chain would either loop or land on an out-of-range
593        // index, both of which we detect explicitly.
594        //
595        // We compute the imbalance BEFORE returning the backing region so
596        // that — even if the eventual `debug_assert!` panics — the backing
597        // chunk is still released. Without this ordering an assertion-on-
598        // leak would *itself* leak the backing region (the asserting drop
599        // unwinds past the deallocate call), upgrading the bug we wanted
600        // to catch into a strictly worse leak.
601        //
602        // **Drop-during-unwind escalation**: the `debug_assert!` below
603        // only fires in debug builds, and only when the caller failed to
604        // free all live slots before drop. If the slab is being dropped
605        // as part of an in-flight panic-unwind AND a slot is leaked AND
606        // we are in a debug build, the assertion's panic-while-panicking
607        // triggers an **immediate process abort** (Rust language rule).
608        // Release builds never assert and never abort here. Treat the
609        // debug-only abort as a louder version of the leak-detection
610        // signal it already is — not as a regression. The condition is
611        // a caller bug (live slots at slab drop); the abort makes the
612        // bug impossible to ignore.
613        #[cfg(debug_assertions)]
614        let imbalance: Option<(u32, u32)> = if core::mem::needs_drop::<T>() {
615            // SAFETY: &mut self — exclusive access; the cells are owned.
616            let next_uncarved = unsafe { *self.next_uncarved.get() };
617            let mut head = unsafe { *self.free_head.get() };
618            let mut freelist_len: u32 = 0;
619            // Bound the walk to capacity to defend against a corrupted
620            // cycle (would otherwise loop forever).
621            while head != 0 && freelist_len <= self.capacity {
622                let slot_idx = head - 1;
623                if slot_idx >= self.capacity {
624                    // Corrupted index — abandon the count; surface as a
625                    // softer assertion below.
626                    break;
627                }
628                let slot_ptr = self.slot_ptr(slot_idx);
629                // SAFETY: slot holds a FreeLink (we put it there in deallocate).
630                let link = unsafe { slot_ptr.cast::<FreeLink>().read() };
631                head = link.next_idx;
632                freelist_len += 1;
633            }
634            if freelist_len == next_uncarved {
635                None
636            } else {
637                Some((next_uncarved, freelist_len))
638            }
639        } else {
640            None
641        };
642        // SAFETY: base and backing_layout came from a single backing.allocate
643        // call in `with_protection`. We hold the only path to either field
644        // (no Clone impl, no exposed mutator). Run the deallocate BEFORE the
645        // assertion so a leaked-T panic does not also leak the backing.
646        // `base_ptr()` recomputes from the (post-move-safe) backing.base()
647        // and the captured offset — same address the construction site
648        // recorded into `base_offset`.
649        unsafe {
650            self.backing
651                .deallocate(self.base_ptr(), self.backing_layout)
652        };
653        #[cfg(debug_assertions)]
654        if let Some((next_uncarved, freelist_len)) = imbalance {
655            debug_assert!(
656                false,
657                "Slab dropped with {} live slot(s) (carved={}, freelist={}). \
658                 Caller failed to deallocate all outstanding `T`s before drop — \
659                 any T: Drop on those slots was leaked.",
660                next_uncarved - freelist_len,
661                next_uncarved,
662                freelist_len,
663            );
664        }
665    }
666}
667
668// Send if all components are Send. !Sync via UnsafeCell.
669unsafe impl<T, B, M> Send for Slab<T, B, M>
670where
671    T: Send,
672    B: Allocator + FixedRange + Send,
673    M: FreelistProtection + Send,
674{
675}
676
677// ============================================================================
678// Kani proof harnesses
679//
680// These prove correctness properties of the freelist push/pop and slot-index
681// recovery logic on a tiny slab. Kani enumerates all input combinations
682// symbolically; the proofs run under `cargo kani` only and are invisible to
683// stable builds.
684// ============================================================================
685
686// Kani proofs depend on `crate::backing::InlineBacked`; the `backing` module is gated
687// behind the `std` feature in this crate (see Cargo.toml), so the proof
688// module must be gated similarly. Kani CI must run with the `std`
689// feature enabled for these proofs to compile.
690#[cfg(all(kani, feature = "std"))]
691mod kani_proofs {
692    use super::*;
693    use crate::backing::InlineBacked;
694
695    /// Allocate-then-deallocate-then-allocate returns the SAME slot
696    /// pointer. This is the LIFO property of the freelist push/pop.
697    #[kani::proof]
698    #[kani::unwind(3)]
699    fn alloc_dealloc_alloc_returns_same_slot() {
700        let s: Slab<u64, InlineBacked<512>, NoProtection> =
701            Slab::new(8, InlineBacked::<512>::new()).unwrap();
702        let layout = NonZeroLayout::for_type::<u64>().unwrap();
703        let a = s.allocate(layout).unwrap().cast::<u8>();
704        let a_ptr = a.as_ptr();
705        unsafe { s.deallocate(a, layout) };
706        let b = s.allocate(layout).unwrap().cast::<u8>();
707        assert!(b.as_ptr() == a_ptr);
708    }
709
710    /// Two distinct live allocations never overlap. (Single-step
711    /// version — full coverage of N allocations would need a loop
712    /// Kani can unwind.)
713    #[kani::proof]
714    #[kani::unwind(3)]
715    fn two_allocs_never_overlap() {
716        let s: Slab<u64, InlineBacked<512>, NoProtection> =
717            Slab::new(8, InlineBacked::<512>::new()).unwrap();
718        let layout = NonZeroLayout::for_type::<u64>().unwrap();
719        let a = s.allocate(layout).unwrap().cast::<u8>();
720        let b = s.allocate(layout).unwrap().cast::<u8>();
721        assert!(a.as_ptr() != b.as_ptr());
722    }
723
724    /// Slot-index recovery from a returned pointer round-trips
725    /// correctly: the index computed from the pointer matches the
726    /// slot the allocator just carved out.
727    #[kani::proof]
728    #[kani::unwind(3)]
729    fn slot_index_round_trips() {
730        let s: Slab<u64, InlineBacked<512>, NoProtection> =
731            Slab::new(8, InlineBacked::<512>::new()).unwrap();
732        let layout = NonZeroLayout::for_type::<u64>().unwrap();
733        let a = s.allocate(layout).unwrap().cast::<u8>();
734        let idx = s.slot_index(a).expect("slot index must resolve");
735        // idx is 0-based; first carved slot is index 0.
736        assert!(idx == 0);
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use crate::backing::InlineBacked;
744
745    /// Test struct to exercise the slab.
746    #[derive(Debug, PartialEq)]
747    struct Foo(u64);
748
749    /// A backing big enough to hold many Foo slots.
750    /// Foo is 8 bytes, FreeLink is 8 bytes, so block_stride = 8.
751    /// 128 slots × 8 bytes = 1024 bytes — fits in InlineBacked<1024>.
752    fn make_slab() -> Slab<Foo, InlineBacked<1024>, NoProtection> {
753        Slab::new(128, InlineBacked::<1024>::new()).unwrap()
754    }
755
756    #[test]
757    fn capacity_matches() {
758        let s = make_slab();
759        assert_eq!(s.capacity(), 128);
760        assert_eq!(s.block_stride(), 8);
761        assert_eq!(s.capacity_bytes(), Some(1024));
762    }
763
764    /// `usable_size` reports the full slot stride, not the requested size, so
765    /// an outer `PoisonOnFree`/`ZeroizeOnFree` scrubs the whole slot on free.
766    /// `Slab<u8>` has a 1-byte type but an 8-byte stride (FreeLink floor).
767    #[test]
768    fn usable_size_reports_full_stride() {
769        let s: Slab<u8, InlineBacked<512>> = Slab::new(8, InlineBacked::<512>::new()).unwrap();
770        assert_eq!(s.block_stride(), 8);
771        let layout = NonZeroLayout::from_size_align(1, 1).unwrap();
772        let block = s.allocate(layout).unwrap();
773        let ptr = block.cast::<u8>();
774        let us = unsafe { s.usable_size(ptr, layout) };
775        assert_eq!(us, Some(8), "usable_size must report the full slot stride");
776        unsafe { s.deallocate(ptr, layout) };
777    }
778
779    #[test]
780    fn allocate_returns_distinct_slots() {
781        let s = make_slab();
782        let layout = NonZeroLayout::for_type::<Foo>().unwrap();
783        let a = s.allocate(layout).unwrap();
784        let b = s.allocate(layout).unwrap();
785        assert_ne!(a.cast::<u8>().as_ptr(), b.cast::<u8>().as_ptr());
786        // Slots are stride-apart.
787        assert_eq!(
788            (b.cast::<u8>().as_ptr() as usize) - (a.cast::<u8>().as_ptr() as usize),
789            8
790        );
791    }
792
793    #[test]
794    fn allocate_then_deallocate_reuses_slot() {
795        let s = make_slab();
796        let layout = NonZeroLayout::for_type::<Foo>().unwrap();
797        let a = s.allocate(layout).unwrap();
798        let a_addr = a.cast::<u8>().as_ptr();
799        unsafe { s.deallocate(a.cast(), layout) };
800        let b = s.allocate(layout).unwrap();
801        // LIFO — the just-freed slot should come back.
802        assert_eq!(a_addr, b.cast::<u8>().as_ptr());
803    }
804
805    #[test]
806    fn allocate_exhausts_capacity() {
807        let s: Slab<u64, InlineBacked<64>, NoProtection> =
808            Slab::new(8, InlineBacked::<64>::new()).unwrap();
809        let layout = NonZeroLayout::for_type::<u64>().unwrap();
810        for _ in 0..8 {
811            assert!(s.allocate(layout).is_ok());
812        }
813        assert!(s.allocate(layout).is_err());
814    }
815
816    #[test]
817    fn allocate_rejects_oversized_layout() {
818        let s = make_slab();
819        let too_big = NonZeroLayout::from_size_align(64, 8).unwrap();
820        assert!(s.allocate(too_big).is_err());
821    }
822
823    #[test]
824    fn allocate_rejects_overaligned_layout() {
825        let s = make_slab();
826        let over_aligned = NonZeroLayout::from_size_align(8, 64).unwrap();
827        assert!(s.allocate(over_aligned).is_err());
828    }
829
830    #[cfg(feature = "std")]
831    #[test]
832    fn alloc_dealloc_alloc_round_trip_many() {
833        let s: Slab<u64, InlineBacked<1024>, NoProtection> =
834            Slab::new(128, InlineBacked::<1024>::new()).unwrap();
835        let layout = NonZeroLayout::for_type::<u64>().unwrap();
836        // Allocate a bunch, free a bunch, re-allocate — all the freed slots
837        // come back from the freelist in LIFO order.
838        let mut ptrs = Vec::new();
839        for _ in 0..64 {
840            ptrs.push(s.allocate(layout).unwrap());
841        }
842        // Free in reverse order.
843        for p in ptrs.iter().rev() {
844            unsafe { s.deallocate(p.cast(), layout) };
845        }
846        // Re-allocate — should get them back in the original order (LIFO).
847        for p in ptrs.iter() {
848            let b = s.allocate(layout).unwrap();
849            assert_eq!(p.cast::<u8>().as_ptr(), b.cast::<u8>().as_ptr());
850        }
851    }
852
853    #[test]
854    fn pow2_stride_shift_is_set() {
855        // u64 stride is 8 (pow2) — shift must be 3.
856        let s: Slab<u64, InlineBacked<64>, NoProtection> =
857            Slab::new(8, InlineBacked::<64>::new()).unwrap();
858        assert_eq!(s.block_stride, 8);
859        assert_eq!(s.stride_shift, 3);
860    }
861
862    #[cfg(feature = "std")]
863    #[test]
864    fn non_pow2_stride_shift_is_zero_sentinel() {
865        // String is 24 bytes on 64-bit — not a power of two. Shift sentinel
866        // is 0, forcing the slow div/mod path in slot_index.
867        let s: Slab<String, InlineBacked<256>, NoProtection> =
868            Slab::new(8, InlineBacked::<256>::new()).unwrap();
869        assert_eq!(s.block_stride, 24);
870        assert_eq!(s.stride_shift, 0);
871        // Round-trip verifies both alloc and dealloc handle the non-pow2 path.
872        let layout = NonZeroLayout::for_type::<String>().unwrap();
873        let a = s.allocate(layout).unwrap();
874        unsafe { s.deallocate(a.cast(), layout) };
875        // After dealloc, the slot is back on the freelist — the next alloc
876        // must return the same pointer (LIFO).
877        let b = s.allocate(layout).unwrap();
878        assert_eq!(a.cast::<u8>().as_ptr(), b.cast::<u8>().as_ptr());
879        // Balance the alloc/dealloc so Slab's debug-only leak check passes
880        // on drop (we never wrote a String into the slot, so it's safe to
881        // free without drop_in_place).
882        unsafe { s.deallocate(b.cast(), layout) };
883    }
884
885    #[test]
886    fn fixed_range_contains_slots() {
887        let s = make_slab();
888        let layout = NonZeroLayout::for_type::<Foo>().unwrap();
889        let a = s.allocate(layout).unwrap();
890        assert!(s.contains(a.cast::<u8>()));
891    }
892
893    #[cfg(feature = "std")]
894    #[test]
895    fn slab_with_string_payload() {
896        // A larger T to verify block_stride > FreeLink size.
897        // String is ~24 bytes on 64-bit.
898        let s: Slab<String, InlineBacked<256>, NoProtection> =
899            Slab::new(8, InlineBacked::<256>::new()).unwrap();
900        let layout = NonZeroLayout::for_type::<String>().unwrap();
901        let a = s.allocate(layout).unwrap();
902        unsafe {
903            a.cast::<String>().as_ptr().write("hello".to_string());
904            // Then drop and free; we never re-read so this is safe.
905            core::ptr::drop_in_place(a.cast::<String>().as_ptr());
906            s.deallocate(a.cast(), layout);
907        }
908    }
909
910    /// Boundary: `Slab::new(0, _)` must fail — a zero-capacity slab is
911    /// useless and would underflow the index math at every `next_uncarved`
912    /// check (we use `>=` so 0 >= 0 correctly fails alloc, but rejecting at
913    /// construction is the documented contract).
914    #[test]
915    fn rejects_zero_capacity() {
916        let r = Slab::<u64, InlineBacked<64>, NoProtection>::new(0, InlineBacked::<64>::new());
917        assert!(r.is_err());
918    }
919
920    // Note: the previous runtime test `rejects_zst_payload` constructed
921    // `Slab::<(), InlineBacked<64>, NoProtection>::new(...)` and expected
922    // `Err(AllocError)`. That rejection was later promoted to a
923    // compile-time const_assert (`Slab::ASSERT_T_NON_ZST`), so the
924    // misuse can no longer be expressed as a runtime test — it would
925    // fail to compile at every call site. The equivalent pin lives as a
926    // `compile_fail` doctest on the `Slab` type's docs (line 98).
927
928    /// `capacity = usize::MAX` triggers either the u32 conversion guard or
929    /// the `block_stride * capacity` overflow guard — never panics.
930    #[test]
931    fn rejects_usize_max_capacity() {
932        let r =
933            Slab::<u64, InlineBacked<64>, NoProtection>::new(usize::MAX, InlineBacked::<64>::new());
934        assert!(r.is_err());
935    }
936
937    /// `T` whose `align_of` is 1 and `size_of` is 1 (e.g. `u8`) — stride
938    /// must round up to `size_of::<FreeLink>() = 8` so a freelist link
939    /// fits in the slot.
940    #[test]
941    fn stride_for_u8_payload_rounds_up_to_freelink_size() {
942        let s: Slab<u8, InlineBacked<128>, NoProtection> =
943            Slab::new(8, InlineBacked::<128>::new()).unwrap();
944        assert_eq!(
945            s.block_stride(),
946            8,
947            "u8 stride must round up to FreeLink size"
948        );
949        // And round-trip an allocation to confirm the freelist can store
950        // a FreeLink in the u8-sized slot.
951        let layout = NonZeroLayout::for_type::<u8>().unwrap();
952        let a = s.allocate(layout).unwrap();
953        unsafe { s.deallocate(a.cast(), layout) };
954        let b = s.allocate(layout).unwrap();
955        assert_eq!(a.cast::<u8>().as_ptr(), b.cast::<u8>().as_ptr());
956        unsafe { s.deallocate(b.cast(), layout) };
957    }
958
959    /// Allocate exactly `capacity` slots; the `capacity+1`-th allocate
960    /// must return `AllocError` (next_uncarved exhaustion).
961    #[test]
962    fn allocate_capacity_plus_one_returns_err() {
963        const CAP: usize = 4;
964        let s: Slab<u64, InlineBacked<64>, NoProtection> =
965            Slab::new(CAP, InlineBacked::<64>::new()).unwrap();
966        let layout = NonZeroLayout::for_type::<u64>().unwrap();
967        // capacity allocs all succeed.
968        for i in 0..CAP {
969            assert!(s.allocate(layout).is_ok(), "slot {i} should succeed");
970        }
971        // The (CAP+1)-th must fail.
972        assert!(
973            s.allocate(layout).is_err(),
974            "alloc past capacity must return AllocError",
975        );
976    }
977
978    /// Boundary: a release-build double-free is documented UB, but the
979    /// debug build's `debug_assert!` on stride alignment catches a
980    /// pointer pulled from a freelist-link slot's `next_idx` byte (which
981    /// is NOT on a stride boundary).
982    ///
983    /// Direct verification: deallocate the same slot twice without
984    /// `debug_assertions` would corrupt; in debug, the slot's `next_idx`
985    /// loops onto itself in the freelist and the next allocate either
986    /// returns the same slot OR loops via the defense-in-depth
987    /// `next_idx > capacity` rejection. We can't assert UB safely, but
988    /// we can check that a single allocate after a (legitimate) free
989    /// returns the LIFO-correct slot.
990    #[test]
991    fn lifo_property_holds_after_alloc_dealloc_realloc() {
992        let s: Slab<u64, InlineBacked<128>, NoProtection> =
993            Slab::new(16, InlineBacked::<128>::new()).unwrap();
994        let layout = NonZeroLayout::for_type::<u64>().unwrap();
995        // Regression: slab.base() must agree with the slab's owned backing's
996        // base. A prior bug stored an absolute pointer captured BEFORE the
997        // backing moved into Self, leaving `slab.base` pointing at the old
998        // location of the InlineBacked's storage. That pointer was stale
999        // for the rest of the slab's life and writes through it landed
1000        // in someone else's stack frame.
1001        use forge_alloc_core::FixedRange;
1002        let backing_storage = s.backing().base();
1003        assert_eq!(
1004            s.base().as_ptr(),
1005            backing_storage.as_ptr(),
1006            "Slab base pointer must agree with current backing.base() — \
1007             stale-pointer bug if not",
1008        );
1009        // Alloc 3, free in reverse, re-alloc in order. Each re-alloc
1010        // returns the most-recently-freed slot.
1011        let a = s.allocate(layout).unwrap();
1012        let b = s.allocate(layout).unwrap();
1013        let c = s.allocate(layout).unwrap();
1014        let a_addr = a.cast::<u8>().as_ptr();
1015        let b_addr = b.cast::<u8>().as_ptr();
1016        let c_addr = c.cast::<u8>().as_ptr();
1017        unsafe {
1018            s.deallocate(a.cast(), layout);
1019            s.deallocate(b.cast(), layout);
1020            s.deallocate(c.cast(), layout);
1021        }
1022        // Free order: a, b, c — so head is c. LIFO: alloc returns c, b, a.
1023        let r1 = s.allocate(layout).unwrap().cast::<u8>().as_ptr();
1024        let r2 = s.allocate(layout).unwrap().cast::<u8>().as_ptr();
1025        let r3 = s.allocate(layout).unwrap().cast::<u8>().as_ptr();
1026        assert_eq!(r1, c_addr);
1027        assert_eq!(r2, b_addr);
1028        assert_eq!(r3, a_addr);
1029    }
1030
1031    #[cfg(feature = "siphasher")]
1032    #[test]
1033    fn siphash_protected_slab_round_trips() {
1034        use forge_alloc_core::SipHashMAC;
1035        let s: Slab<u64, InlineBacked<1024>, SipHashMAC> = Slab::with_protection(
1036            128,
1037            InlineBacked::<1024>::new(),
1038            SipHashMAC::with_key([0x42; 16]),
1039        )
1040        .unwrap();
1041        let layout = NonZeroLayout::for_type::<u64>().unwrap();
1042        let a = s.allocate(layout).unwrap();
1043        unsafe { s.deallocate(a.cast(), layout) };
1044        let b = s.allocate(layout).unwrap();
1045        assert_eq!(a.cast::<u8>().as_ptr(), b.cast::<u8>().as_ptr());
1046    }
1047
1048    /// Regression: the freelist MAC must survive the slab being MOVED between a
1049    /// free and a later alloc. `InlineBacked`'s storage is structure-relative,
1050    /// so a move changes every slot's absolute address — the MAC nonce is the
1051    /// slot INDEX (move-invariant). Were it the free-time absolute address (the
1052    /// bug this guards), popping the freed slot after the move would false-fail
1053    /// verification, bump `corruption_events`, leak the slot, and carve a fresh
1054    /// one instead of reusing slot 0.
1055    #[cfg(feature = "siphasher")]
1056    #[test]
1057    fn siphash_freelist_survives_move() {
1058        use forge_alloc_core::SipHashMAC;
1059        let s: Slab<u64, InlineBacked<1024>, SipHashMAC> = Slab::with_protection(
1060            128,
1061            InlineBacked::<1024>::new(),
1062            SipHashMAC::with_key([0x99; 16]),
1063        )
1064        .unwrap();
1065        let layout = NonZeroLayout::for_type::<u64>().unwrap();
1066        let a = s.allocate(layout).unwrap();
1067        assert_eq!(s.slot_index(a.cast()), Some(0));
1068        unsafe { s.deallocate(a.cast(), layout) };
1069        // Move the slab; the slot region relocates.
1070        let moved = s;
1071        let b = moved.allocate(layout).unwrap();
1072        assert_eq!(
1073            moved.corruption_events(),
1074            0,
1075            "freelist MAC must not false-fail across a move",
1076        );
1077        assert_eq!(
1078            moved.slot_index(b.cast()),
1079            Some(0),
1080            "freed slot 0 must be reused, not abandoned to a false corruption",
1081        );
1082    }
1083}