Skip to main content

hyperlight_common/virtq/
pool.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 The Hyperlight Authors.
3//! Buffer pool implementations for virtqueue buffer management.
4//!
5//! This module provides concrete buffer allocators:
6//!
7//! - [`BufferPool`] - a two-tier run allocator for variable-sized allocations.
8//! - [`RecyclePool`] - a single-tier fixed-slot free-list recycler for bounded
9//!   descriptor segments.
10//!
11//! All implement [`BufferProvider`] from the [`super::buffer`] module.
12//!
13//! # BufferPool design
14//!
15//! `BufferPool` is a variable-sized run allocator.
16//!
17//! # Two-tier layout
18//!
19//! [`BufferPool`] divides the underlying region into two slabs with different
20//! slot sizes:
21//!
22//! - The lower tier (default `L = 256`) is intended for *smaller allocations* -
23//!   control messages, descriptor metadata, and other small structures. Small
24//!   allocations first try this tier.
25//! - The upper tier (default `U = 4096`) uses page sized slots and is intended
26//!   for larger contiguous buffers.
27
28use alloc::rc::Rc;
29use core::cell::RefCell;
30use core::ops::Deref;
31
32use fixedbitset::FixedBitSet;
33use smallvec::SmallVec;
34
35use super::buffer::{AllocError, Allocation, BufferProvider};
36
37/// Wrapper asserting `Send` for an inner value that is only ever accessed from
38/// a single thread.
39///
40/// [`BufferPool`] and [`RecyclePool`] hold their state in an `Rc<RefCell<..>>`,
41/// which is neither `Send` nor `Sync`. Their allocations are exposed as
42/// zero-copy reply payloads through
43/// [`Bytes::from_owner`](bytes::Bytes::from_owner), whose owner bound is
44/// `Send + 'static`; this wrapper exists solely so the pools can satisfy that
45/// bound.
46///
47/// # Safety
48///
49/// The `Send` assertion is only sound while the wrapped value - and every
50/// `Bytes` handed out from it - stays on a single thread. Hyperlight guests are
51/// single-threaded, so this holds for guest-side use. It is unsound to move a
52/// pool (or a reply `Bytes`) to another thread, e.g. by using these pools with a
53/// producer/consumer on the multi-threaded host.
54#[derive(Debug)]
55struct SendWrap<T>(T);
56
57impl<T: Clone> Clone for SendWrap<T> {
58    fn clone(&self) -> Self {
59        Self(self.0.clone())
60    }
61}
62
63impl<T> Deref for SendWrap<T> {
64    type Target = T;
65    fn deref(&self) -> &T {
66        &self.0
67    }
68}
69
70#[derive(Debug, Clone)]
71struct Slab<const N: usize> {
72    base_addr: u64,
73    used_slots: FixedBitSet,
74    run_starts: FixedBitSet,
75    last_free_run: Option<Allocation>,
76}
77
78impl<const N: usize> Slab<N> {
79    fn new(base_addr: u64, region_len: usize) -> Result<Self, AllocError> {
80        let usable = region_len - (region_len % N);
81        let num_slots = usable / N;
82        let used_slots = FixedBitSet::with_capacity(num_slots);
83        let run_starts = FixedBitSet::with_capacity(num_slots);
84
85        if !base_addr.is_multiple_of(N as u64) {
86            return Err(AllocError::InvalidAlign(base_addr));
87        }
88        if num_slots == 0 {
89            return Err(AllocError::EmptyRegion);
90        }
91
92        Ok(Self {
93            base_addr,
94            used_slots,
95            run_starts,
96            last_free_run: None,
97        })
98    }
99
100    fn addr_of(&self, slot_idx: usize) -> Option<u64> {
101        self.base_addr
102            .checked_add((slot_idx as u64).checked_mul(N as u64)?)
103    }
104
105    fn slot_of(&self, addr: u64) -> usize {
106        let off = (addr - self.base_addr) as usize;
107        off / N
108    }
109
110    fn checked_slot_of(&self, addr: u64, len: usize) -> Result<usize, AllocError> {
111        if addr < self.base_addr {
112            return Err(AllocError::InvalidFree(addr, len));
113        }
114
115        let off = (addr - self.base_addr) as usize;
116        if !off.is_multiple_of(N) {
117            return Err(AllocError::InvalidFree(addr, len));
118        }
119
120        let slot = off / N;
121        if slot >= self.used_slots.len() {
122            return Err(AllocError::InvalidFree(addr, len));
123        }
124
125        Ok(slot)
126    }
127
128    fn live_run_slots_at(&self, start: usize) -> Option<usize> {
129        if start >= self.used_slots.len()
130            || !self.used_slots.contains(start)
131            || !self.run_starts.contains(start)
132        {
133            return None;
134        }
135
136        let mut end = start + 1;
137        while end < self.used_slots.len()
138            && self.used_slots.contains(end)
139            && !self.run_starts.contains(end)
140        {
141            end += 1;
142        }
143
144        Some(end - start)
145    }
146
147    fn maybe_invalidate_last_run(&mut self, alloc: Allocation) {
148        if let Some(run) = &self.last_free_run {
149            let new_end = alloc.addr + alloc.len as u64;
150            let run_end = run.addr + run.len as u64;
151
152            if alloc.addr < run_end && run.addr < new_end {
153                self.last_free_run = None;
154            }
155        }
156    }
157
158    fn find_slots(&mut self, slots_num: usize) -> Option<usize> {
159        debug_assert!(slots_num > 0);
160
161        if let Some(alloc) = self.last_free_run
162            && alloc.len >= slots_num * N
163        {
164            let pos = self.slot_of(alloc.addr);
165            let _ = self.last_free_run.take();
166            return Some(pos);
167        }
168
169        let total = self.used_slots.len();
170        self.used_slots.zeroes().find(|&next_free| {
171            let end = next_free + slots_num;
172            end <= total && self.used_slots.count_zeroes(next_free..end) == slots_num
173        })
174    }
175
176    fn alloc(&mut self, len: usize) -> Result<Allocation, AllocError> {
177        if len == 0 {
178            return Err(AllocError::InvalidArg);
179        }
180
181        let total = self.used_slots.len();
182        let need_slots = len.div_ceil(N);
183        if need_slots > total {
184            return Err(AllocError::OutOfMemory);
185        }
186
187        let idx = self.find_slots(need_slots).ok_or(AllocError::NoSpace)?;
188        self.used_slots.insert_range(idx..idx + need_slots);
189        self.run_starts.insert(idx);
190        let addr = self.addr_of(idx).ok_or(AllocError::Overflow)?;
191
192        let alloc = Allocation {
193            addr,
194            len: need_slots * N,
195        };
196
197        self.maybe_invalidate_last_run(alloc);
198        Ok(alloc)
199    }
200
201    fn dealloc_addr(&mut self, addr: u64) -> Result<(), AllocError> {
202        let start = self.checked_slot_of(addr, 0)?;
203        let run_slots = self
204            .live_run_slots_at(start)
205            .ok_or(AllocError::InvalidFree(addr, 0))?;
206        self.dealloc_run(start, run_slots, addr)
207    }
208
209    fn dealloc_run(&mut self, start: usize, run_slots: usize, addr: u64) -> Result<(), AllocError> {
210        let len = run_slots * N;
211        self.used_slots.remove_range(start..start + run_slots);
212        self.run_starts.set(start, false);
213        self.last_free_run = Some(Allocation { addr, len });
214        Ok(())
215    }
216
217    fn allocation_len(&self, addr: u64) -> Result<usize, AllocError> {
218        let start = self.checked_slot_of(addr, 0)?;
219        let run_slots = self
220            .live_run_slots_at(start)
221            .ok_or(AllocError::InvalidFree(addr, 0))?;
222        Ok(run_slots * N)
223    }
224
225    fn capacity(&self) -> usize {
226        self.used_slots.len() * N
227    }
228
229    fn range(&self) -> core::ops::Range<u64> {
230        self.base_addr..self.base_addr + self.capacity() as u64
231    }
232
233    fn contains(&self, addr: u64) -> bool {
234        self.range().contains(&addr)
235    }
236
237    fn reset(&mut self) {
238        self.used_slots.clear();
239        self.run_starts.clear();
240        self.last_free_run = None;
241    }
242}
243
244#[cfg(test)]
245impl<const N: usize> Slab<N> {
246    fn free_bytes(&self) -> usize {
247        (self.used_slots.len() - self.used_slots.count_ones(..)) * N
248    }
249}
250
251#[inline]
252fn align_up(val: usize, align: usize) -> Result<usize, AllocError> {
253    if align == 0 {
254        return Err(AllocError::InvalidArg);
255    }
256
257    val.checked_next_multiple_of(align)
258        .ok_or(AllocError::Overflow)
259}
260
261#[derive(Debug)]
262struct Inner<const L: usize, const U: usize> {
263    lower: Slab<L>,
264    upper: Slab<U>,
265}
266
267// SAFETY: only sound for single-threaded (guest-side) access; see the
268// type-level invariant on `SendWrap`.
269unsafe impl<const L: usize, const U: usize> Send for SendWrap<Rc<RefCell<Inner<L, U>>>> {}
270
271/// Two tier buffer pool with small and large slabs.
272#[derive(Debug, Clone)]
273pub struct BufferPool<const L: usize = 256, const U: usize = 4096> {
274    inner: SendWrap<Rc<RefCell<Inner<L, U>>>>,
275}
276
277impl<const L: usize, const U: usize> BufferPool<L, U> {
278    /// Create a new buffer pool over a fixed region.
279    pub fn new(base_addr: u64, region_len: usize) -> Result<Self, AllocError> {
280        let inner = Inner::<L, U>::new(base_addr, region_len)?;
281        Ok(Self {
282            inner: SendWrap(Rc::new(RefCell::new(inner))),
283        })
284    }
285}
286
287impl BufferPool {
288    /// Upper slab slot size in bytes.
289    pub const fn upper_slot_size() -> usize {
290        4096
291    }
292
293    /// Lower slab slot size in bytes.
294    pub const fn lower_slot_size() -> usize {
295        256
296    }
297}
298
299#[cfg(all(test, loom))]
300#[derive(Debug, Clone)]
301pub struct BufferPoolSync<const L: usize = 256, const U: usize = 4096> {
302    inner: std::sync::Arc<std::sync::Mutex<Inner<L, U>>>,
303}
304
305#[cfg(all(test, loom))]
306impl<const L: usize, const U: usize> BufferPoolSync<L, U> {
307    /// Create a new buffer pool over a fixed region.
308    pub fn new(base_addr: u64, region_len: usize) -> Result<Self, AllocError> {
309        let inner = Inner::<L, U>::new(base_addr, region_len)?;
310        Ok(Self {
311            inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
312        })
313    }
314}
315
316impl<const L: usize, const U: usize> Inner<L, U> {
317    /// Create a new buffer pool over a fixed region.
318    pub fn new(base_addr: u64, region_len: usize) -> Result<Self, AllocError> {
319        const LOWER_FRACTION: usize = 8;
320
321        let base = usize::try_from(base_addr).map_err(|_| AllocError::Overflow)?;
322        let region_end = base.checked_add(region_len).ok_or(AllocError::Overflow)?;
323
324        let lower_base = align_up(base, L)?;
325        let usable = region_end
326            .checked_sub(lower_base)
327            .ok_or(AllocError::EmptyRegion)?;
328
329        let lower_region = usable / LOWER_FRACTION;
330        let lower = Slab::<L>::new(lower_base as u64, lower_region)?;
331
332        let upper_base = lower_base
333            .checked_add(lower.capacity())
334            .ok_or(AllocError::Overflow)?;
335
336        let upper_base = align_up(upper_base, U)?;
337        let upper_region = region_end
338            .checked_sub(upper_base)
339            .ok_or(AllocError::EmptyRegion)?;
340
341        let upper = Slab::<U>::new(upper_base as u64, upper_region)?;
342        Ok(Self { lower, upper })
343    }
344
345    /// Allocate at least `len` bytes.
346    pub fn alloc(&mut self, len: usize) -> Result<Allocation, AllocError> {
347        if len <= L {
348            match self.lower.alloc(len) {
349                Ok(alloc) => return Ok(alloc),
350                Err(AllocError::NoSpace) => {}
351                Err(e) => return Err(e),
352            }
353        }
354
355        // fallback to upper slab
356        self.upper.alloc(len)
357    }
358
359    /// Free a previously allocated block by its start address.
360    pub fn dealloc_addr(&mut self, addr: u64) -> Result<(), AllocError> {
361        if self.lower.contains(addr) {
362            self.lower.dealloc_addr(addr)
363        } else {
364            self.upper.dealloc_addr(addr)
365        }
366    }
367
368    /// Capacity of a live allocation by its start address.
369    pub fn allocation_len(&self, addr: u64) -> Result<usize, AllocError> {
370        if self.lower.contains(addr) {
371            self.lower.allocation_len(addr)
372        } else {
373            self.upper.allocation_len(addr)
374        }
375    }
376}
377
378impl<const L: usize, const U: usize> BufferProvider for BufferPool<L, U> {
379    fn max_alloc_len(&self) -> usize {
380        U
381    }
382
383    fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
384        self.inner.borrow_mut().alloc(len)
385    }
386
387    fn alloc_sg(&self, total_len: usize) -> Result<SmallVec<[Allocation; 4]>, AllocError> {
388        Ok(smallvec::smallvec![self.alloc(total_len)?])
389    }
390
391    fn dealloc(&self, addr: u64) -> Result<(), AllocError> {
392        self.inner.borrow_mut().dealloc_addr(addr)
393    }
394
395    fn reset(&self) {
396        let mut inner = self.inner.borrow_mut();
397        inner.lower.reset();
398        inner.upper.reset();
399    }
400}
401
402impl<const L: usize, const U: usize> BufferPool<L, U> {
403    /// Free a previously allocated block by its start address.
404    pub fn dealloc_addr(&self, addr: u64) -> Result<(), AllocError> {
405        self.inner.borrow_mut().dealloc_addr(addr)
406    }
407
408    /// Capacity of a live allocation by its start address.
409    pub fn allocation_len(&self, addr: u64) -> Result<usize, AllocError> {
410        self.inner.borrow().allocation_len(addr)
411    }
412}
413
414#[cfg(all(test, loom))]
415impl<const L: usize, const U: usize> BufferProvider for BufferPoolSync<L, U> {
416    fn max_alloc_len(&self) -> usize {
417        U
418    }
419
420    fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
421        self.inner.lock().expect("poisoned mutex").alloc(len)
422    }
423
424    fn alloc_sg(&self, total_len: usize) -> Result<SmallVec<[Allocation; 4]>, AllocError> {
425        Ok(smallvec::smallvec![self.alloc(total_len)?])
426    }
427
428    fn dealloc(&self, addr: u64) -> Result<(), AllocError> {
429        self.inner
430            .lock()
431            .expect("poisoned mutex")
432            .dealloc_addr(addr)
433    }
434}
435
436/// Single-tier fixed-slot free list.
437///
438/// Tracks a fixed set of equal-sized buffer slots. Allocation pops a free slot
439/// and deallocation returns it, both O(1). A [`FixedBitSet`] records which slots
440/// are currently allocated, so double frees and frees of unknown addresses are
441/// rejected without scanning the free list.
442struct RecycleList {
443    base_addr: u64,
444    slot_size: usize,
445    count: usize,
446    /// Free slot addresses, popped/pushed LIFO.
447    free: SmallVec<[u64; 64]>,
448    /// One bit per slot index; set means the slot is currently handed out.
449    allocated: FixedBitSet,
450}
451
452// SAFETY: only sound for single-threaded (guest-side) access; see the
453// type-level invariant on `SendWrap`.
454unsafe impl Send for SendWrap<Rc<RefCell<RecycleList>>> {}
455
456impl RecycleList {
457    fn new(base_addr: u64, region_len: usize, slot_size: usize) -> Result<Self, AllocError> {
458        if slot_size == 0 {
459            return Err(AllocError::InvalidArg);
460        }
461
462        let count = region_len / slot_size;
463        if count == 0 {
464            return Err(AllocError::EmptyRegion);
465        }
466
467        let mut free = SmallVec::with_capacity(count);
468        for i in 0..count {
469            free.push(base_addr + (i * slot_size) as u64);
470        }
471
472        Ok(Self {
473            base_addr,
474            slot_size,
475            count,
476            free,
477            allocated: FixedBitSet::with_capacity(count),
478        })
479    }
480
481    fn end(&self) -> u64 {
482        self.base_addr + (self.count * self.slot_size) as u64
483    }
484
485    fn contains(&self, addr: u64) -> bool {
486        (self.base_addr..self.end()).contains(&addr)
487    }
488
489    /// Validate that `addr` names a slot start within the region.
490    fn slot_of(&self, addr: u64) -> Result<usize, AllocError> {
491        if !self.contains(addr) {
492            return Err(AllocError::InvalidFree(addr, 0));
493        }
494
495        let off = addr - self.base_addr;
496        if !off.is_multiple_of(self.slot_size as u64) {
497            return Err(AllocError::InvalidFree(addr, 0));
498        }
499
500        Ok((off / self.slot_size as u64) as usize)
501    }
502
503    /// Validate that `addr` is a live (currently allocated) slot start.
504    fn live_slot_of(&self, addr: u64) -> Result<usize, AllocError> {
505        let slot = self.slot_of(addr)?;
506        if !self.allocated.contains(slot) {
507            return Err(AllocError::InvalidFree(addr, 0));
508        }
509        Ok(slot)
510    }
511
512    fn alloc(&mut self, len: usize) -> Result<Allocation, AllocError> {
513        if len == 0 {
514            return Err(AllocError::InvalidArg);
515        }
516        if len > self.slot_size {
517            return Err(AllocError::OutOfMemory);
518        }
519
520        let addr = self.free.pop().ok_or(AllocError::NoSpace)?;
521        // Safety of the index: `addr` came from `free`, which only ever holds
522        // valid slot starts.
523        self.allocated
524            .insert(((addr - self.base_addr) / self.slot_size as u64) as usize);
525
526        Ok(Allocation {
527            addr,
528            len: self.slot_size,
529        })
530    }
531
532    fn dealloc_addr(&mut self, addr: u64) -> Result<(), AllocError> {
533        let slot = self.live_slot_of(addr)?;
534        self.allocated.set(slot, false);
535        self.free.push(addr);
536        Ok(())
537    }
538
539    fn allocation_len(&self, addr: u64) -> Result<usize, AllocError> {
540        self.live_slot_of(addr)?;
541        Ok(self.slot_size)
542    }
543
544    /// Rebuild state so that exactly the addresses in `allocated` are marked
545    /// live and every other slot is free.
546    ///
547    /// On error the pool is left in an indeterminate state and should be
548    /// [`reset`](Self::reset) before reuse.
549    fn restore_allocated(&mut self, allocated: &[u64]) -> Result<(), AllocError> {
550        self.allocated.clear();
551        for &addr in allocated {
552            let slot = self.slot_of(addr)?;
553            if self.allocated.contains(slot) {
554                return Err(AllocError::InvalidFree(addr, self.slot_size));
555            }
556            self.allocated.insert(slot);
557        }
558        self.rebuild_free();
559        Ok(())
560    }
561
562    fn reset(&mut self) {
563        self.allocated.clear();
564        self.rebuild_free();
565    }
566
567    /// Repopulate the free list with every slot whose allocated bit is clear.
568    fn rebuild_free(&mut self) {
569        self.free.clear();
570        for i in 0..self.count {
571            if !self.allocated.contains(i) {
572                self.free.push(self.base_addr + (i * self.slot_size) as u64);
573            }
574        }
575    }
576
577    fn slot_addr(&self, index: usize) -> Option<u64> {
578        (index < self.count).then(|| self.base_addr + (index * self.slot_size) as u64)
579    }
580
581    fn num_free(&self) -> usize {
582        self.free.len()
583    }
584}
585
586/// A recycling buffer provider with fixed-size slots.
587///
588/// Holds a fixed set of equal-sized buffer addresses in a free list. Alloc and
589/// dealloc are O(1). It is intended for bounded scatter/gather descriptor
590/// segments that are pre-allocated and recycled after use:
591/// [`alloc_sg`](BufferProvider::alloc_sg) splits a logical payload into
592/// `ceil(total_len / slot_size)` fixed-size segments.
593#[derive(Clone)]
594pub struct RecyclePool {
595    inner: SendWrap<Rc<RefCell<RecycleList>>>,
596}
597
598impl RecyclePool {
599    /// Create a recycling pool of `slot_size`-byte slots over a fixed region.
600    ///
601    /// The base address is aligned up to `slot_size`; the slot count is based
602    /// on the remaining usable region after alignment.
603    pub fn new(base_addr: u64, region_len: usize, slot_size: usize) -> Result<Self, AllocError> {
604        if slot_size == 0 {
605            return Err(AllocError::InvalidArg);
606        }
607
608        let base = usize::try_from(base_addr).map_err(|_| AllocError::Overflow)?;
609        let region_end = base.checked_add(region_len).ok_or(AllocError::Overflow)?;
610        let aligned = align_up(base, slot_size)?;
611        let usable = region_end
612            .checked_sub(aligned)
613            .ok_or(AllocError::EmptyRegion)?;
614        let list = RecycleList::new(aligned as u64, usable, slot_size)?;
615
616        Ok(Self {
617            inner: SendWrap(Rc::new(RefCell::new(list))),
618        })
619    }
620
621    /// Rebuild pool state so that every address in `allocated` is removed from
622    /// the free list, matching externally known inflight state.
623    pub fn restore_allocated(&self, allocated: &[u64]) -> Result<(), AllocError> {
624        self.inner.borrow_mut().restore_allocated(allocated)
625    }
626
627    /// Compute the address of slot `index`.
628    ///
629    /// Returns `None` if `index >= count`.
630    pub fn slot_addr(&self, index: usize) -> Option<u64> {
631        self.inner.borrow().slot_addr(index)
632    }
633
634    /// Number of free slots.
635    pub fn num_free(&self) -> usize {
636        self.inner.borrow().num_free()
637    }
638
639    /// Free a previously allocated slot by address.
640    pub fn dealloc_addr(&self, addr: u64) -> Result<(), AllocError> {
641        self.inner.borrow_mut().dealloc_addr(addr)
642    }
643
644    /// Capacity of a live allocation by its start address.
645    pub fn allocation_len(&self, addr: u64) -> Result<usize, AllocError> {
646        self.inner.borrow().allocation_len(addr)
647    }
648
649    /// Base address of the pool region.
650    pub fn base_addr(&self) -> u64 {
651        self.inner.borrow().base_addr
652    }
653
654    /// Slot size in bytes.
655    pub fn slot_size(&self) -> usize {
656        self.inner.borrow().slot_size
657    }
658
659    /// Number of slots in the pool.
660    pub fn count(&self) -> usize {
661        self.inner.borrow().count
662    }
663}
664
665impl BufferProvider for RecyclePool {
666    fn max_alloc_len(&self) -> usize {
667        self.inner.borrow().slot_size
668    }
669
670    fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
671        self.inner.borrow_mut().alloc(len)
672    }
673
674    fn dealloc(&self, addr: u64) -> Result<(), AllocError> {
675        self.inner.borrow_mut().dealloc_addr(addr)
676    }
677
678    fn reset(&self) {
679        self.inner.borrow_mut().reset()
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    fn make_pool<const L: usize, const U: usize>(size: usize) -> BufferPool<L, U> {
688        let base = align_up(0x10000, L.max(U)).unwrap() as u64;
689        BufferPool::<L, U>::new(base, size).unwrap()
690    }
691
692    fn make_recycle_pool(slot_count: usize, slot_size: usize) -> RecyclePool {
693        let base = 0x80000u64;
694        RecyclePool::new(base, slot_count * slot_size, slot_size).unwrap()
695    }
696
697    #[test]
698    fn test_pool_new_success() {
699        let pool = BufferPool::<256, 4096>::new(0x10000, 1024 * 1024).unwrap();
700        assert!(pool.inner.borrow().lower.capacity() > 0);
701        assert!(pool.inner.borrow().upper.capacity() > 0);
702    }
703
704    #[test]
705    fn test_pool_alloc_small_to_lower() {
706        let pool = make_pool::<256, 4096>(1024 * 1024);
707        let alloc = pool.alloc(128).unwrap();
708
709        // Should come from lower slab
710        assert!(pool.inner.borrow().lower.contains(alloc.addr));
711        assert_eq!(alloc.len, 256);
712    }
713
714    #[test]
715    fn test_pool_alloc_large_to_upper() {
716        let pool = make_pool::<256, 4096>(1024 * 1024);
717        let alloc = pool.alloc(1500).unwrap();
718
719        // Should come from upper slab
720        assert!(pool.inner.borrow().upper.contains(alloc.addr));
721        assert_eq!(alloc.len, 4096);
722    }
723
724    #[test]
725    fn test_pool_alloc_fallback_to_upper() {
726        let pool = make_pool::<256, 4096>(1024 * 1024);
727
728        // Fill lower slab completely
729        let mut allocations = Vec::new();
730        while pool.inner.borrow().lower.free_bytes() > 0 {
731            allocations.push(pool.inner.borrow_mut().lower.alloc(256).unwrap());
732        }
733
734        // Small allocation should fallback to upper slab
735        let alloc = pool.alloc(128).unwrap();
736        assert!(pool.inner.borrow().upper.contains(alloc.addr));
737    }
738
739    #[test]
740    fn test_pool_free_from_lower() {
741        let pool = make_pool::<256, 4096>(1024 * 1024);
742        let alloc = pool.alloc(128).unwrap();
743
744        let free_before = pool.inner.borrow().lower.free_bytes();
745        pool.dealloc(alloc.addr).unwrap();
746        assert_eq!(
747            pool.inner.borrow().lower.free_bytes(),
748            free_before + alloc.len
749        );
750    }
751
752    #[test]
753    fn test_pool_free_from_upper() {
754        let pool = make_pool::<256, 4096>(1024 * 1024);
755        let alloc = pool.alloc(1500).unwrap();
756
757        let free_before = pool.inner.borrow().upper.free_bytes();
758        pool.dealloc(alloc.addr).unwrap();
759        assert_eq!(
760            pool.inner.borrow().upper.free_bytes(),
761            free_before + alloc.len
762        );
763    }
764
765    #[test]
766    fn test_pool_stress_many_allocations() {
767        let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
768        let mut allocations = Vec::new();
769
770        // Allocate many buffers
771        for i in 0..100 {
772            let size = if i % 2 == 0 { 128 } else { 1500 };
773            allocations.push(pool.alloc(size).unwrap());
774        }
775
776        // Free half of them
777        for i in (0..100).step_by(2) {
778            pool.dealloc(allocations[i].addr).unwrap();
779        }
780
781        // Should be able to allocate again
782        for i in 0..50 {
783            let size = if i % 2 == 0 { 128 } else { 1500 };
784            let _alloc = pool.alloc(size).unwrap();
785        }
786    }
787
788    #[test]
789    fn test_pool_mixed_workload() {
790        let pool = make_pool::<256, 4096>(2 * 1024 * 1024);
791
792        // Simulate virtio-net workload
793        let desc_buf = pool.alloc(64).unwrap(); // Control message
794        let rx_buf1 = pool.alloc(1500).unwrap(); // MTU packet
795        let rx_buf2 = pool.alloc(1500).unwrap(); // MTU packet
796        let tx_buf = pool.alloc(4096).unwrap(); // Large buffer
797
798        // Free and reallocate
799        pool.dealloc(rx_buf1.addr).unwrap();
800        let rx_buf3 = pool.alloc(1500).unwrap();
801
802        // Should reuse freed buffer (LIFO)
803        assert_eq!(rx_buf3.addr, rx_buf1.addr);
804
805        pool.dealloc(desc_buf.addr).unwrap();
806        pool.dealloc(rx_buf2.addr).unwrap();
807        pool.dealloc(rx_buf3.addr).unwrap();
808        pool.dealloc(tx_buf.addr).unwrap();
809    }
810
811    #[test]
812    fn test_pool_zero_allocation_error() {
813        let pool = make_pool::<256, 4096>(1024 * 1024);
814        let result = pool.alloc(0);
815        assert!(matches!(result, Err(AllocError::InvalidArg)));
816    }
817
818    #[test]
819    fn test_pool_too_large_allocation() {
820        let pool = make_pool::<256, 4096>(1024 * 1024);
821        let result = pool.alloc(2 * 1024 * 1024); // Larger than pool
822        assert!(matches!(result, Err(AllocError::OutOfMemory)));
823    }
824
825    #[test]
826    fn test_align_up_helper() {
827        assert_eq!(align_up(0, 256).unwrap(), 0);
828        assert_eq!(align_up(1, 256).unwrap(), 256);
829        assert_eq!(align_up(256, 256).unwrap(), 256);
830        assert_eq!(align_up(257, 256).unwrap(), 512);
831        assert_eq!(align_up(511, 256).unwrap(), 512);
832        assert_eq!(align_up(512, 256).unwrap(), 512);
833        assert!(matches!(align_up(1, 0), Err(AllocError::InvalidArg)));
834        assert!(matches!(
835            align_up(usize::MAX, 256),
836            Err(AllocError::Overflow)
837        ));
838    }
839
840    #[test]
841    fn test_recycle_pool_alignment_subtracts_padding() {
842        let pool = RecyclePool::new(0x80001, 8192, 4096).unwrap();
843
844        assert_eq!(pool.base_addr(), 0x81000);
845        assert_eq!(pool.count(), 1);
846    }
847
848    // Edge case: allocation exactly at boundary
849    #[test]
850    fn test_pool_boundary_allocation() {
851        let pool = make_pool::<256, 4096>(1024 * 1024);
852
853        // Allocate exactly at boundary
854        let alloc = pool.alloc(256).unwrap();
855        assert!(pool.inner.borrow().lower.contains(alloc.addr));
856
857        // Allocate just over boundary
858        let alloc2 = pool.alloc(257).unwrap();
859        assert!(pool.inner.borrow().upper.contains(alloc2.addr));
860    }
861
862    #[test]
863    fn test_buffer_pool_reset_returns_to_initial_state() {
864        let pool = make_pool::<256, 4096>(0x20000);
865
866        // Allocate from both tiers
867        let a1 = pool.inner.borrow_mut().alloc(128).unwrap();
868        let a2 = pool.inner.borrow_mut().alloc(4096).unwrap();
869        assert!(a1.len > 0);
870        assert!(a2.len > 0);
871
872        pool.reset();
873
874        let inner = pool.inner.borrow();
875        assert_eq!(inner.lower.free_bytes(), inner.lower.capacity());
876        assert_eq!(inner.upper.free_bytes(), inner.upper.capacity());
877    }
878
879    #[test]
880    fn test_buffer_pool_reset_allows_reallocation() {
881        let pool = make_pool::<256, 4096>(0x20000);
882
883        // Fill up some allocations
884        let mut allocs = Vec::new();
885        for _ in 0..5 {
886            allocs.push(pool.inner.borrow_mut().alloc(256).unwrap());
887        }
888
889        pool.reset();
890
891        // Should be able to allocate as if fresh
892        let a = pool.inner.borrow_mut().alloc(256).unwrap();
893        assert!(a.len > 0);
894    }
895
896    #[test]
897    fn test_pool_dealloc_addr_routes_to_correct_tier() {
898        let pool = make_pool::<256, 4096>(0x20000);
899        let lower = pool.alloc(128).unwrap();
900        let upper = pool.alloc(1024).unwrap();
901
902        assert_eq!(pool.allocation_len(lower.addr).unwrap(), 256);
903        assert_eq!(pool.allocation_len(upper.addr).unwrap(), 4096);
904
905        pool.dealloc_addr(lower.addr).unwrap();
906        pool.dealloc_addr(upper.addr).unwrap();
907    }
908
909    #[test]
910    fn test_buffer_pool_alloc_sg_uses_one_contiguous_run() {
911        let pool = make_pool::<256, 4096>(0x20000);
912        let sgs = pool.alloc_sg(4096 * 2 + 1).unwrap();
913
914        assert_eq!(sgs.len(), 1);
915        assert_eq!(sgs[0].len, 4096 * 3);
916
917        for sg in sgs {
918            pool.dealloc(sg.addr).unwrap();
919        }
920    }
921
922    #[test]
923    fn test_buffer_pool_alloc_sg_large_run() {
924        let pool = make_pool::<256, 4096>(0x20000);
925        let sgs = pool.alloc_sg(8192).unwrap();
926
927        assert_eq!(sgs.len(), 1);
928        assert_eq!(sgs[0].len, 8192);
929
930        for sg in sgs {
931            pool.dealloc(sg.addr).unwrap();
932        }
933    }
934
935    #[test]
936    fn test_recycle_pool_alloc_sg_splits() {
937        let pool = make_recycle_pool(8, 4096);
938        let sgs = pool.alloc_sg(4096 * 2 + 1).unwrap();
939
940        assert_eq!(sgs.len(), 3);
941        assert_eq!(sgs[0].len, 4096);
942        assert_eq!(sgs[1].len, 4096);
943        assert_eq!(sgs[2].len, 4096);
944
945        for sg in sgs {
946            pool.dealloc(sg.addr).unwrap();
947        }
948    }
949
950    #[test]
951    fn test_recycle_pool_restore_allocated_removes_from_free_list() {
952        let pool = make_recycle_pool(4, 4096);
953        assert_eq!(pool.num_free(), 4);
954
955        let addrs = [0x80000, 0x81000]; // slots 0 and 1
956        pool.restore_allocated(&addrs).unwrap();
957        assert_eq!(pool.num_free(), 2);
958
959        // Allocating should only return the two remaining slots
960        let a1 = pool.alloc(4096).unwrap();
961        let a2 = pool.alloc(4096).unwrap();
962        assert!(pool.alloc(4096).is_err());
963
964        // The allocated addresses should be the non-restored ones
965        let mut got = [a1.addr, a2.addr];
966        got.sort();
967        assert_eq!(got, [0x82000, 0x83000]);
968    }
969
970    #[test]
971    fn test_recycle_pool_restore_allocated_invalid_addr_returns_error() {
972        let pool = make_recycle_pool(4, 4096);
973        let result = pool.restore_allocated(&[0xDEAD]);
974        assert!(result.is_err());
975    }
976
977    #[test]
978    fn test_recycle_pool_restore_allocated_then_dealloc_roundtrip() {
979        let pool = make_recycle_pool(4, 4096);
980        let addr = 0x81000u64;
981
982        pool.restore_allocated(&[addr]).unwrap();
983        assert_eq!(pool.num_free(), 3);
984
985        // Dealloc the restored address
986        pool.dealloc(addr).unwrap();
987        assert_eq!(pool.num_free(), 4);
988    }
989
990    #[test]
991    fn test_recycle_pool_restore_allocated_all_slots() {
992        let pool = make_recycle_pool(4, 4096);
993        let addrs: Vec<u64> = (0..4).map(|i| 0x80000 + i * 4096).collect();
994
995        pool.restore_allocated(&addrs).unwrap();
996        assert_eq!(pool.num_free(), 0);
997        assert!(pool.alloc(4096).is_err());
998    }
999
1000    #[test]
1001    fn test_recycle_pool_restore_allocated_empty_list_is_noop() {
1002        let pool = make_recycle_pool(4, 4096);
1003        pool.restore_allocated(&[]).unwrap();
1004        assert_eq!(pool.num_free(), 4);
1005    }
1006
1007    #[test]
1008    fn test_recycle_pool_restore_allocated_resets_first() {
1009        let pool = make_recycle_pool(4, 4096);
1010
1011        // Allocate some slots
1012        let _ = pool.alloc(4096).unwrap();
1013        let _ = pool.alloc(4096).unwrap();
1014        assert_eq!(pool.num_free(), 2);
1015
1016        // restore_allocated resets then removes - so 4 - 1 = 3
1017        pool.restore_allocated(&[0x80000]).unwrap();
1018        assert_eq!(pool.num_free(), 3);
1019    }
1020
1021    #[test]
1022    fn test_recycle_pool_dealloc_out_of_range() {
1023        let pool = make_recycle_pool(4, 4096);
1024        let _ = pool.alloc(4096).unwrap();
1025
1026        assert!(matches!(
1027            pool.dealloc(0xDEAD),
1028            Err(AllocError::InvalidFree(0xDEAD, 0))
1029        ));
1030    }
1031
1032    #[test]
1033    fn test_recycle_pool_dealloc_misaligned() {
1034        let pool = make_recycle_pool(4, 4096);
1035        let _ = pool.alloc(4096).unwrap();
1036
1037        assert!(matches!(
1038            pool.dealloc(0x80001),
1039            Err(AllocError::InvalidFree(0x80001, 0))
1040        ));
1041    }
1042
1043    #[test]
1044    fn test_recycle_pool_dealloc_double_free() {
1045        let pool = make_recycle_pool(4, 4096);
1046        let a = pool.alloc(4096).unwrap();
1047        pool.dealloc(a.addr).unwrap();
1048
1049        // Second dealloc should fail - address is already in the free list
1050        assert!(matches!(
1051            pool.dealloc(a.addr),
1052            Err(AllocError::InvalidFree(_, _))
1053        ));
1054    }
1055
1056    #[test]
1057    fn test_recycle_pool_alloc_sg_rolls_back_on_failure() {
1058        let pool = make_recycle_pool(2, 4096);
1059
1060        assert!(matches!(pool.alloc_sg(4096 * 3), Err(AllocError::NoSpace)));
1061        assert_eq!(pool.num_free(), 2);
1062
1063        let alloc = pool.alloc(4096).unwrap();
1064        assert_eq!(pool.num_free(), 1);
1065        pool.dealloc(alloc.addr).unwrap();
1066    }
1067
1068    #[test]
1069    fn test_recycle_pool_dealloc_addr_and_allocation_len() {
1070        let pool = make_recycle_pool(4, 4096);
1071        let alloc = pool.alloc(4096).unwrap();
1072
1073        assert_eq!(pool.allocation_len(alloc.addr).unwrap(), 4096);
1074        pool.dealloc_addr(alloc.addr).unwrap();
1075        assert!(matches!(
1076            pool.allocation_len(alloc.addr),
1077            Err(AllocError::InvalidFree(_, 0))
1078        ));
1079    }
1080
1081    #[test]
1082    fn test_recycle_pool_random_order_dealloc() {
1083        let pool = make_recycle_pool(8, 4096);
1084
1085        let mut allocs: Vec<Allocation> = (0..8).map(|_| pool.alloc(4096).unwrap()).collect();
1086        assert_eq!(pool.num_free(), 0);
1087
1088        // Dealloc in reverse order
1089        allocs.reverse();
1090        for a in &allocs {
1091            pool.dealloc(a.addr).unwrap();
1092        }
1093        assert_eq!(pool.num_free(), 8);
1094
1095        // All slots should be re-allocatable
1096        let reallocs: Vec<Allocation> = (0..8).map(|_| pool.alloc(4096).unwrap()).collect();
1097        assert_eq!(pool.num_free(), 0);
1098
1099        // Verify all addresses are distinct
1100        let mut addrs: Vec<u64> = reallocs.iter().map(|a| a.addr).collect();
1101        addrs.sort();
1102        addrs.dedup();
1103        assert_eq!(addrs.len(), 8);
1104    }
1105
1106    #[test]
1107    fn test_recycle_pool_interleaved_alloc_dealloc_order() {
1108        let pool = make_recycle_pool(4, 4096);
1109
1110        let a0 = pool.alloc(4096).unwrap();
1111        let a1 = pool.alloc(4096).unwrap();
1112        let a2 = pool.alloc(4096).unwrap();
1113        let a3 = pool.alloc(4096).unwrap();
1114        assert_eq!(pool.num_free(), 0);
1115
1116        // Free middle slots first (out of allocation order)
1117        pool.dealloc(a2.addr).unwrap();
1118        pool.dealloc(a0.addr).unwrap();
1119        assert_eq!(pool.num_free(), 2);
1120
1121        // Re-alloc gets the out-of-order slots back (LIFO)
1122        let b0 = pool.alloc(4096).unwrap();
1123        assert_eq!(b0.addr, a0.addr);
1124        let b1 = pool.alloc(4096).unwrap();
1125        assert_eq!(b1.addr, a2.addr);
1126
1127        // Free everything in yet another order
1128        pool.dealloc(a1.addr).unwrap();
1129        pool.dealloc(b0.addr).unwrap();
1130        pool.dealloc(b1.addr).unwrap();
1131        pool.dealloc(a3.addr).unwrap();
1132        assert_eq!(pool.num_free(), 4);
1133
1134        // All 4 original addresses should be available
1135        let mut final_addrs: Vec<u64> = (0..4).map(|_| pool.alloc(4096).unwrap().addr).collect();
1136        final_addrs.sort();
1137        let expected: Vec<u64> = (0..4).map(|i| 0x80000 + i * 4096).collect();
1138        assert_eq!(final_addrs, expected);
1139    }
1140
1141    #[test]
1142    fn test_recycle_pool_dealloc_order_independent_of_alloc_order() {
1143        let pool = make_recycle_pool(6, 256);
1144
1145        // Allocate all
1146        let allocs: Vec<Allocation> = (0..6).map(|_| pool.alloc(256).unwrap()).collect();
1147
1148        // Dealloc in scattered order: 4, 1, 5, 0, 3, 2
1149        let order = [4, 1, 5, 0, 3, 2];
1150        for &i in &order {
1151            pool.dealloc(allocs[i].addr).unwrap();
1152        }
1153        assert_eq!(pool.num_free(), 6);
1154
1155        // Re-allocate all and verify we get back the full set
1156        let mut realloc_addrs: Vec<u64> = (0..6).map(|_| pool.alloc(256).unwrap().addr).collect();
1157        realloc_addrs.sort();
1158
1159        let mut orig_addrs: Vec<u64> = allocs.iter().map(|a| a.addr).collect();
1160        orig_addrs.sort();
1161
1162        assert_eq!(realloc_addrs, orig_addrs);
1163    }
1164}
1165
1166#[cfg(test)]
1167mod fuzz {
1168    use quickcheck::{Arbitrary, Gen, QuickCheck};
1169
1170    use super::*;
1171
1172    const MAX_OPS: usize = 10;
1173    const MAX_ALLOC_SIZE: usize = 8192;
1174
1175    #[derive(Clone, Debug)]
1176    enum Op {
1177        Alloc(usize),
1178        AllocSg(usize),
1179        Dealloc(usize),
1180    }
1181
1182    impl Arbitrary for Op {
1183        fn arbitrary(g: &mut Gen) -> Self {
1184            match u8::arbitrary(g) % 3 {
1185                0 => Op::Alloc(usize::arbitrary(g) % MAX_ALLOC_SIZE + 1),
1186                1 => Op::AllocSg(usize::arbitrary(g) % MAX_ALLOC_SIZE + 1),
1187                2 => Op::Dealloc(usize::arbitrary(g)),
1188                _ => unreachable!(),
1189            }
1190        }
1191    }
1192
1193    #[derive(Clone, Debug)]
1194    struct Scenario {
1195        pool_size: usize,
1196        ops: Vec<Op>,
1197    }
1198
1199    impl Arbitrary for Scenario {
1200        fn arbitrary(g: &mut Gen) -> Self {
1201            let pool_size = (usize::arbitrary(g) % (4 * 1024 * 1024)) + (1024 * 1024);
1202            let num_ops = usize::arbitrary(g) % MAX_OPS + 1;
1203            let ops = (0..num_ops).map(|_| Op::arbitrary(g)).collect();
1204
1205            Scenario { pool_size, ops }
1206        }
1207    }
1208
1209    fn run_scenario(s: Scenario) -> bool {
1210        let base = align_up(0x10000, 4096).unwrap() as u64;
1211        let pool = match BufferPool::<256, 4096>::new(base, s.pool_size) {
1212            Ok(p) => p,
1213            Err(_) => return true,
1214        };
1215
1216        let mut allocations: Vec<Allocation> = Vec::new();
1217
1218        for op in &s.ops {
1219            match op {
1220                Op::Alloc(size) => match pool.alloc(*size) {
1221                    Ok(alloc) => {
1222                        assert!(alloc.len >= *size);
1223                        allocations.push(alloc);
1224                    }
1225                    Err(AllocError::NoSpace | AllocError::OutOfMemory) => {}
1226                    Err(_) => {
1227                        return false;
1228                    }
1229                },
1230                Op::AllocSg(size) => match pool.alloc_sg(*size) {
1231                    Ok(sgs) => {
1232                        let total: usize = sgs.iter().map(|sg| sg.len).sum();
1233                        assert!(total >= *size);
1234                        allocations.extend(sgs);
1235                    }
1236                    Err(AllocError::NoSpace | AllocError::OutOfMemory) => {}
1237                    Err(_) => {
1238                        return false;
1239                    }
1240                },
1241                Op::Dealloc(idx) => {
1242                    if allocations.is_empty() {
1243                        continue;
1244                    }
1245
1246                    let idx = idx % allocations.len();
1247                    let alloc = allocations.swap_remove(idx);
1248
1249                    match pool.dealloc(alloc.addr) {
1250                        Ok(_) => {}
1251                        Err(_) => return false,
1252                    }
1253                }
1254            }
1255
1256            if check_pool_invariants(&pool, &allocations).is_err() {
1257                return false;
1258            }
1259        }
1260
1261        // Cleanup
1262        for alloc in &allocations {
1263            if pool.dealloc(alloc.addr).is_err() {
1264                return false;
1265            }
1266        }
1267
1268        check_pool_invariants(&pool, &allocations).is_ok()
1269    }
1270
1271    fn check_slab_invariants<const N: usize>(slab: &Slab<N>) -> Result<(), &'static str> {
1272        let used = slab.used_slots.count_ones(..);
1273        let free = slab.used_slots.count_zeroes(..);
1274        if used + free != slab.used_slots.len() {
1275            return Err("used + free != total slots");
1276        }
1277
1278        let expected_free = free * N;
1279        if slab.free_bytes() != expected_free {
1280            return Err("free_bytes doesn't match bitmap");
1281        }
1282
1283        if let Some(alloc) = slab.last_free_run {
1284            if alloc.len == 0 || alloc.len % N != 0 {
1285                return Err("last_free_run has invalid length");
1286            }
1287            if !slab.contains(alloc.addr) {
1288                return Err("last_free_run addr outside range");
1289            }
1290        }
1291
1292        Ok(())
1293    }
1294
1295    fn check_pool_invariants<const L: usize, const U: usize>(
1296        pool: &BufferPool<L, U>,
1297        allocations: &[Allocation],
1298    ) -> Result<(), &'static str> {
1299        check_slab_invariants(&pool.inner.borrow().lower)?;
1300        check_slab_invariants(&pool.inner.borrow().upper)?;
1301
1302        if pool.inner.borrow().lower.range().end > pool.inner.borrow().upper.range().start {
1303            return Err("lower and upper ranges overlap");
1304        }
1305
1306        let mut seen = std::collections::HashSet::new();
1307
1308        for alloc in allocations {
1309            if !pool.inner.borrow().lower.contains(alloc.addr)
1310                && !pool.inner.borrow().upper.contains(alloc.addr)
1311            {
1312                return Err("allocation address outside pool ranges");
1313            }
1314
1315            if alloc.len % L != 0 && alloc.len % U != 0 {
1316                return Err("allocation length not aligned to any tier");
1317            }
1318
1319            if !seen.insert(alloc.addr) {
1320                return Err("duplicate allocation address in tracking");
1321            }
1322        }
1323
1324        Ok(())
1325    }
1326
1327    #[test]
1328    fn prop_allocator_invariants() {
1329        #[cfg(miri)]
1330        let tests = 10;
1331        #[cfg(not(miri))]
1332        let tests = 1000;
1333
1334        QuickCheck::new()
1335            .tests(tests)
1336            .quickcheck(run_scenario as fn(Scenario) -> bool);
1337    }
1338}