Skip to main content

hyperlight_common/virtq/
pool.rs

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