Skip to main content

rvm_memory/
allocator.rs

1//! Buddy allocator for physical page allocation (ADR-136).
2//!
3//! A `no_std`, `no_alloc` buddy allocator that uses a fixed-size bitmap
4//! to track allocation state. Each bit in the bitmap represents a block
5//! at its corresponding order level. The allocator manages blocks in
6//! power-of-two sizes (in pages).
7//!
8//! ## Design
9//!
10//! The allocator uses a single flat bitmap where each order level owns
11//! a contiguous range of bits. For order `k`, there are `total_pages / 2^k`
12//! blocks. A set bit means the block is free.
13//!
14//! Block splitting and merging (buddy coalescing) are performed during
15//! `alloc_pages` and `free_pages` respectively.
16
17use rvm_types::{PhysAddr, RvmError, RvmResult};
18
19use crate::PAGE_SIZE;
20
21/// Maximum supported order (allocation of `2^MAX_ORDER` pages at once).
22const MAX_ORDER: usize = 10; // Up to 1024 pages = 4 MiB per block.
23
24/// Compute the number of bitmap words needed for a given number of bits.
25const fn words_for_bits(bits: usize) -> usize {
26    bits.div_ceil(64)
27}
28
29/// Total bitmap bits needed for all order levels given `total_pages`.
30///
31/// For order 0: `total_pages` bits.
32/// For order 1: `total_pages / 2` bits.
33/// ...
34/// For order k: `total_pages / 2^k` bits.
35/// Total = `total_pages * 2 - 1` (geometric series, approximately).
36const fn total_bitmap_bits(total_pages: usize) -> usize {
37    let mut bits = 0;
38    let mut order = 0;
39    while order <= MAX_ORDER {
40        bits += total_pages >> order;
41        order += 1;
42    }
43    bits
44}
45
46/// A buddy allocator managing `TOTAL_PAGES` of physical memory.
47///
48/// The allocator is entirely stack-allocated with a fixed-size bitmap.
49/// `TOTAL_PAGES` must be a power of two and at most `2^MAX_ORDER * some_count`.
50///
51/// # Type Parameters
52///
53/// - `TOTAL_PAGES`: The total number of 4 KiB pages managed. Must be a power of two.
54/// - `BITMAP_WORDS`: The number of `u64` words in the bitmap. Must be at least
55///   `total_bitmap_bits(TOTAL_PAGES) / 64 + 1`. Use [`BuddyAllocator::REQUIRED_BITMAP_WORDS`]
56///   to compute this.
57pub struct BuddyAllocator<const TOTAL_PAGES: usize, const BITMAP_WORDS: usize> {
58    /// Base physical address of the managed memory region.
59    base: PhysAddr,
60    /// Bitmap: bit set = block is free.
61    bitmap: [u64; BITMAP_WORDS],
62    /// Pre-computed cumulative bit offsets per order level.
63    /// `bit_offsets[k]` = sum of `TOTAL_PAGES >> i` for i in 0..k.
64    /// Replaces the O(order) loop in `bit_offset()` with O(1) lookup.
65    bit_offsets: [usize; MAX_ORDER + 1],
66}
67
68impl<const TOTAL_PAGES: usize, const BITMAP_WORDS: usize>
69    BuddyAllocator<TOTAL_PAGES, BITMAP_WORDS>
70{
71    /// The number of bitmap `u64` words required for `TOTAL_PAGES`.
72    pub const REQUIRED_BITMAP_WORDS: usize = words_for_bits(total_bitmap_bits(TOTAL_PAGES));
73
74    /// Create a new buddy allocator managing memory starting at `base`.
75    ///
76    /// All blocks are initially marked as free. `base` must be page-aligned.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`RvmError::AlignmentError`] if `base` is not page-aligned.
81    /// Returns [`RvmError::ResourceLimitExceeded`] if `BITMAP_WORDS` is insufficient.
82    pub fn new(base: PhysAddr) -> RvmResult<Self> {
83        if !base.is_page_aligned() {
84            return Err(RvmError::AlignmentError);
85        }
86        // Verify BITMAP_WORDS is sufficient.
87        if BITMAP_WORDS < Self::REQUIRED_BITMAP_WORDS {
88            return Err(RvmError::ResourceLimitExceeded);
89        }
90
91        // Pre-compute cumulative bit offsets for each order level.
92        let mut bit_offsets = [0usize; MAX_ORDER + 1];
93        let mut cumulative = 0;
94        let mut o = 0;
95        while o <= MAX_ORDER {
96            bit_offsets[o] = cumulative;
97            cumulative += TOTAL_PAGES >> o;
98            o += 1;
99        }
100
101        let mut alloc = Self {
102            base,
103            bitmap: [0u64; BITMAP_WORDS],
104            bit_offsets,
105        };
106        alloc.init_free_all();
107        Ok(alloc)
108    }
109
110    /// Initialize the allocator by marking the highest-order blocks as free.
111    ///
112    /// Only the coarsest level blocks are free initially; smaller blocks are
113    /// split on demand during allocation.
114    fn init_free_all(&mut self) {
115        // Clear entire bitmap first.
116        self.bitmap.fill(0);
117
118        // Mark all blocks at the maximum possible order as free.
119        let max_usable_order = Self::max_usable_order();
120        let block_count = TOTAL_PAGES >> max_usable_order;
121        for blk in 0..block_count {
122            self.set_free(max_usable_order, blk);
123        }
124    }
125
126    /// Return the maximum usable order (capped by `MAX_ORDER` and `TOTAL_PAGES`).
127    const fn max_usable_order() -> usize {
128        let mut order = MAX_ORDER;
129        // Ensure we don't exceed total pages.
130        while order > 0 && (1usize << order) > TOTAL_PAGES {
131            order -= 1;
132        }
133        order
134    }
135
136    /// Allocate `2^order` contiguous pages.
137    ///
138    /// Returns the base `PhysAddr` of the allocated block.
139    ///
140    /// Uses `trailing_zeros` on bitmap words for fast first-free-block
141    /// scanning: O(1) per 64-bit word instead of checking bit-by-bit.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`RvmError::OutOfMemory`] if no block of the requested size
146    /// is available.
147    pub fn alloc_pages(&mut self, order: usize) -> RvmResult<PhysAddr> {
148        if order > Self::max_usable_order() {
149            return Err(RvmError::OutOfMemory);
150        }
151
152        // Try to find a free block at this order using trailing_zeros scan.
153        if let Some(blk) = self.find_first_free(order) {
154            self.clear_free(order, blk);
155            let page_offset = blk << order;
156            let addr = self.base.as_u64() + (page_offset as u64 * PAGE_SIZE as u64);
157            return Ok(PhysAddr::new(addr));
158        }
159
160        // No free block at this order -- try to split a higher-order block.
161        let mut split_order = order + 1;
162        while split_order <= Self::max_usable_order() {
163            if let Some(blk) = self.find_first_free(split_order) {
164                // Remove the block from the higher order.
165                self.clear_free(split_order, blk);
166
167                // Split down to the requested order.
168                let mut current_order = split_order;
169                let mut current_blk = blk;
170                while current_order > order {
171                    current_order -= 1;
172                    // The block at `current_order` splits into two children.
173                    let left_child = current_blk * 2;
174                    let right_child = left_child + 1;
175                    // Mark the right (buddy) child as free.
176                    self.set_free(current_order, right_child);
177                    // Continue splitting the left child.
178                    current_blk = left_child;
179                }
180
181                let page_offset = current_blk << order;
182                let addr = self.base.as_u64() + (page_offset as u64 * PAGE_SIZE as u64);
183                return Ok(PhysAddr::new(addr));
184            }
185
186            split_order += 1;
187        }
188
189        Err(RvmError::OutOfMemory)
190    }
191
192    /// Free a previously allocated block of `2^order` pages starting at `addr`.
193    ///
194    /// The caller must ensure `addr` was returned by a prior `alloc_pages(order)` call.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`RvmError::AlignmentError`] if the address is invalid or misaligned.
199    /// Returns [`RvmError::InvalidTierTransition`] if the order exceeds the maximum.
200    /// Returns [`RvmError::InternalError`] on double-free detection.
201    pub fn free_pages(&mut self, addr: PhysAddr, order: usize) -> RvmResult<()> {
202        if order > Self::max_usable_order() {
203            return Err(RvmError::InvalidTierTransition);
204        }
205        if addr.as_u64() < self.base.as_u64() {
206            return Err(RvmError::AlignmentError);
207        }
208
209        let offset_bytes = addr.as_u64() - self.base.as_u64();
210        if offset_bytes % (PAGE_SIZE as u64) != 0 {
211            return Err(RvmError::AlignmentError);
212        }
213        #[allow(clippy::cast_possible_truncation)]
214        let page_offset = (offset_bytes / PAGE_SIZE as u64) as usize;
215        if page_offset >= TOTAL_PAGES {
216            return Err(RvmError::AlignmentError);
217        }
218
219        let block_index = page_offset >> order;
220
221        // Check alignment: the block must start at a block-aligned offset.
222        if (block_index << order) != page_offset {
223            return Err(RvmError::AlignmentError);
224        }
225
226        // Double-free check: the block should not already be free at this
227        // order, nor should any ancestor block be free (which would mean this
228        // block was coalesced into a larger free block).
229        if self.is_block_free(order, block_index) {
230            return Err(RvmError::InternalError);
231        }
232
233        // Mark the block as free and coalesce with buddy if possible.
234        self.set_free(order, block_index);
235        self.coalesce(order, block_index);
236
237        Ok(())
238    }
239
240    /// Return the total number of free pages across all orders.
241    #[must_use]
242    pub fn free_page_count(&self) -> usize {
243        let mut count = 0;
244        let max_order = Self::max_usable_order();
245        let mut order = 0;
246        while order <= max_order {
247            let block_count = TOTAL_PAGES >> order;
248            for blk in 0..block_count {
249                if self.is_free(order, blk) {
250                    count += 1 << order;
251                }
252            }
253            order += 1;
254        }
255        count
256    }
257
258    /// Coalesce freed blocks with their buddies up the order chain.
259    fn coalesce(&mut self, order: usize, block_index: usize) {
260        let mut current_order = order;
261        let mut current_blk = block_index;
262
263        while current_order < Self::max_usable_order() {
264            let buddy = current_blk ^ 1; // XOR with 1 gives the buddy index.
265            let block_count = TOTAL_PAGES >> current_order;
266
267            if buddy >= block_count {
268                break; // Buddy is out of range.
269            }
270
271            if !self.is_free(current_order, buddy) {
272                break; // Buddy is not free, cannot coalesce.
273            }
274
275            // Remove both blocks from the current order.
276            self.clear_free(current_order, current_blk);
277            self.clear_free(current_order, buddy);
278
279            // Merge into the parent block.
280            current_order += 1;
281            current_blk /= 2;
282            self.set_free(current_order, current_blk);
283        }
284    }
285
286    /// Check if a block is effectively free -- either directly marked free
287    /// at its order, or covered by a free ancestor at a higher order.
288    fn is_block_free(&self, order: usize, block_index: usize) -> bool {
289        if self.is_free(order, block_index) {
290            return true;
291        }
292        // Walk up the ancestor chain.
293        let mut o = order + 1;
294        let mut blk = block_index / 2;
295        while o <= Self::max_usable_order() {
296            if self.is_free(o, blk) {
297                return true;
298            }
299            o += 1;
300            blk /= 2;
301        }
302        false
303    }
304
305    // --- Bitmap helpers ---
306
307    /// Find the first free block at the given order using `trailing_zeros`
308    /// on bitmap words for O(1) per 64-bit word scanning.
309    ///
310    /// Returns the block index, or `None` if no free block exists.
311    fn find_first_free(&self, order: usize) -> Option<usize> {
312        let block_count = TOTAL_PAGES >> order;
313        if block_count == 0 {
314            return None;
315        }
316        let base_bit = self.bit_offsets[order];
317        let start_word = base_bit / 64;
318        let start_bit_in_word = base_bit % 64;
319
320        // Total bits to scan for this order level.
321        let mut remaining = block_count;
322        let mut word_idx = start_word;
323        let mut bit_offset_in_level = 0usize;
324
325        // Handle the first (potentially partial) word.
326        if start_bit_in_word != 0 && word_idx < BITMAP_WORDS {
327            // Mask off bits below our start position in this word.
328            let mask = self.bitmap[word_idx] >> start_bit_in_word;
329            if mask != 0 {
330                let tz = mask.trailing_zeros() as usize;
331                if tz < remaining && (start_bit_in_word + tz) < 64 {
332                    return Some(tz);
333                }
334            }
335            let bits_in_first_word = 64 - start_bit_in_word;
336            let consumed = bits_in_first_word.min(remaining);
337            remaining = remaining.saturating_sub(consumed);
338            bit_offset_in_level += consumed;
339            word_idx += 1;
340        }
341
342        // Scan full 64-bit words using trailing_zeros.
343        while remaining > 0 && word_idx < BITMAP_WORDS {
344            let word = self.bitmap[word_idx];
345            if word != 0 {
346                let tz = word.trailing_zeros() as usize;
347                if tz < remaining.min(64) {
348                    return Some(bit_offset_in_level + tz);
349                }
350            }
351            let consumed = remaining.min(64);
352            remaining -= consumed;
353            bit_offset_in_level += consumed;
354            word_idx += 1;
355        }
356
357        None
358    }
359
360    /// Compute the bit offset for block `blk` at `order`.
361    /// Uses the pre-computed LUT for O(1) instead of O(order) loop.
362    #[inline]
363    fn bit_offset(&self, order: usize, blk: usize) -> usize {
364        self.bit_offsets[order] + blk
365    }
366
367    /// Check if a block is marked as free in the bitmap.
368    #[inline]
369    fn is_free(&self, order: usize, blk: usize) -> bool {
370        let bit = self.bit_offset(order, blk);
371        let word = bit / 64;
372        let bit_in_word = bit % 64;
373        if word >= BITMAP_WORDS {
374            return false;
375        }
376        (self.bitmap[word] >> bit_in_word) & 1 == 1
377    }
378
379    /// Mark a block as free in the bitmap.
380    #[inline]
381    fn set_free(&mut self, order: usize, blk: usize) {
382        let bit = self.bit_offset(order, blk);
383        let word = bit / 64;
384        let bit_in_word = bit % 64;
385        if word < BITMAP_WORDS {
386            self.bitmap[word] |= 1u64 << bit_in_word;
387        }
388    }
389
390    /// Mark a block as allocated (not free) in the bitmap.
391    #[inline]
392    fn clear_free(&mut self, order: usize, blk: usize) {
393        let bit = self.bit_offset(order, blk);
394        let word = bit / 64;
395        let bit_in_word = bit % 64;
396        if word < BITMAP_WORDS {
397            self.bitmap[word] &= !(1u64 << bit_in_word);
398        }
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    /// A small allocator managing 16 pages (64 KiB) for testing.
407    /// Total bitmap bits: 16 + 8 + 4 + 2 + 1 = 31 bits -> 1 word.
408    /// But we need more for the full `MAX_ORDER`=10 chain. Use 2 words.
409    type SmallAllocator = BuddyAllocator<16, 2>;
410
411    fn base() -> PhysAddr {
412        PhysAddr::new(0x1000_0000)
413    }
414
415    #[test]
416    fn create_allocator() {
417        let alloc = SmallAllocator::new(base()).unwrap();
418        assert_eq!(alloc.free_page_count(), 16);
419    }
420
421    #[test]
422    fn unaligned_base_fails() {
423        assert!(matches!(
424            SmallAllocator::new(PhysAddr::new(0x1000_0001)),
425            Err(RvmError::AlignmentError)
426        ));
427    }
428
429    #[test]
430    fn alloc_single_page() {
431        let mut alloc = SmallAllocator::new(base()).unwrap();
432        let addr = alloc.alloc_pages(0).unwrap();
433        assert!(addr.is_page_aligned());
434        assert!(addr.as_u64() >= base().as_u64());
435        assert_eq!(alloc.free_page_count(), 15);
436    }
437
438    #[test]
439    fn alloc_all_pages_individually() {
440        let mut alloc = SmallAllocator::new(base()).unwrap();
441        let mut addrs = [PhysAddr::new(0); 16];
442        for (i, addr) in addrs.iter_mut().enumerate() {
443            *addr = alloc.alloc_pages(0).unwrap();
444            let _ = i;
445        }
446        assert_eq!(alloc.free_page_count(), 0);
447
448        // Next allocation should fail.
449        assert_eq!(alloc.alloc_pages(0), Err(RvmError::OutOfMemory));
450
451        // All addresses should be distinct and page-aligned.
452        for (i, a) in addrs.iter().enumerate() {
453            assert!(a.is_page_aligned());
454            for b in &addrs[(i + 1)..] {
455                assert_ne!(a, b);
456            }
457        }
458    }
459
460    #[test]
461    fn alloc_order_2() {
462        let mut alloc = SmallAllocator::new(base()).unwrap();
463        // Order 2 = 4 pages.
464        let addr = alloc.alloc_pages(2).unwrap();
465        assert!(addr.is_page_aligned());
466        assert_eq!(alloc.free_page_count(), 12);
467    }
468
469    #[test]
470    fn alloc_too_large_fails() {
471        let mut alloc = SmallAllocator::new(base()).unwrap();
472        // `MAX_ORDER` for 16 pages is 4 (2^4 = 16).
473        // Trying order 5 should fail since 2^5 = 32 > 16.
474        assert_eq!(alloc.alloc_pages(5), Err(RvmError::OutOfMemory));
475    }
476
477    #[test]
478    fn free_and_realloc() {
479        let mut alloc = SmallAllocator::new(base()).unwrap();
480        let addr = alloc.alloc_pages(0).unwrap();
481        assert_eq!(alloc.free_page_count(), 15);
482
483        alloc.free_pages(addr, 0).unwrap();
484        assert_eq!(alloc.free_page_count(), 16);
485
486        // Should be able to allocate again.
487        let addr2 = alloc.alloc_pages(0).unwrap();
488        assert!(addr2.is_page_aligned());
489    }
490
491    #[test]
492    fn free_invalid_address() {
493        let mut alloc = SmallAllocator::new(base()).unwrap();
494        // Address before base.
495        assert!(alloc.free_pages(PhysAddr::new(0), 0).is_err());
496        // Unaligned address.
497        assert!(alloc
498            .free_pages(PhysAddr::new(base().as_u64() + 1), 0)
499            .is_err());
500    }
501
502    #[test]
503    fn double_free_detected() {
504        let mut alloc = SmallAllocator::new(base()).unwrap();
505        let addr = alloc.alloc_pages(0).unwrap();
506        alloc.free_pages(addr, 0).unwrap();
507        // Second free should fail.
508        assert_eq!(alloc.free_pages(addr, 0), Err(RvmError::InternalError));
509    }
510
511    #[test]
512    fn buddy_coalescing() {
513        let mut alloc = SmallAllocator::new(base()).unwrap();
514
515        // Allocate two order-0 blocks (consecutive pages).
516        let a = alloc.alloc_pages(0).unwrap();
517        let b = alloc.alloc_pages(0).unwrap();
518        assert_eq!(alloc.free_page_count(), 14);
519
520        // Free both -- they should coalesce into an order-1 block.
521        alloc.free_pages(a, 0).unwrap();
522        alloc.free_pages(b, 0).unwrap();
523        assert_eq!(alloc.free_page_count(), 16);
524
525        // Verify we can now allocate a single order-4 (16-page) block,
526        // meaning everything coalesced back to the top.
527        let big = alloc.alloc_pages(4).unwrap();
528        assert!(big.is_page_aligned());
529        assert_eq!(alloc.free_page_count(), 0);
530    }
531
532    #[test]
533    fn alloc_mixed_orders() {
534        let mut alloc = SmallAllocator::new(base()).unwrap();
535
536        // Allocate: 1 page + 2 pages + 4 pages + 8 pages = 15 pages.
537        // Only 1 page should remain.
538        let _a = alloc.alloc_pages(0).unwrap(); // 1 page
539        let _b = alloc.alloc_pages(1).unwrap(); // 2 pages
540        let _c = alloc.alloc_pages(2).unwrap(); // 4 pages
541        let _d = alloc.alloc_pages(3).unwrap(); // 8 pages
542        assert_eq!(alloc.free_page_count(), 1);
543
544        // One more order-0 allocation should succeed.
545        let _e = alloc.alloc_pages(0).unwrap();
546        assert_eq!(alloc.free_page_count(), 0);
547
548        // Now should be out of memory.
549        assert_eq!(alloc.alloc_pages(0), Err(RvmError::OutOfMemory));
550    }
551
552    /// A larger allocator for stress testing: 256 pages.
553    type MediumAllocator = BuddyAllocator<256, 16>;
554
555    #[test]
556    fn medium_allocator_full_cycle() {
557        let mut alloc = MediumAllocator::new(base()).unwrap();
558        assert_eq!(alloc.free_page_count(), 256);
559
560        // Allocate 64 order-0 blocks.
561        let mut addrs = [PhysAddr::new(0); 64];
562        for addr in &mut addrs {
563            *addr = alloc.alloc_pages(0).unwrap();
564        }
565        assert_eq!(alloc.free_page_count(), 192);
566
567        // Free them all.
568        for addr in &addrs {
569            alloc.free_pages(*addr, 0).unwrap();
570        }
571        assert_eq!(alloc.free_page_count(), 256);
572    }
573
574    // ---------------------------------------------------------------
575    // Buddy allocator under full allocation pressure
576    // ---------------------------------------------------------------
577
578    #[test]
579    fn full_allocation_pressure_order_0() {
580        // Allocate all 16 pages one by one, then verify OOM.
581        let mut alloc = SmallAllocator::new(base()).unwrap();
582        let mut addrs = [PhysAddr::new(0); 16];
583        for addr in &mut addrs {
584            *addr = alloc.alloc_pages(0).unwrap();
585        }
586        assert_eq!(alloc.free_page_count(), 0);
587        assert_eq!(alloc.alloc_pages(0), Err(RvmError::OutOfMemory));
588
589        // Free one and immediately re-allocate.
590        alloc.free_pages(addrs[7], 0).unwrap();
591        assert_eq!(alloc.free_page_count(), 1);
592        let reused = alloc.alloc_pages(0).unwrap();
593        assert!(reused.is_page_aligned());
594        assert_eq!(alloc.free_page_count(), 0);
595    }
596
597    #[test]
598    fn full_allocation_pressure_mixed_orders() {
599        // Allocate: 8 pages (order 3), 4 pages (order 2), 2 pages (order 1),
600        // 1 page (order 0), 1 page (order 0) = 16 total.
601        let mut alloc = SmallAllocator::new(base()).unwrap();
602        let addr_8pg = alloc.alloc_pages(3).unwrap(); // 8 pages
603        let addr_4pg = alloc.alloc_pages(2).unwrap(); // 4 pages
604        let addr_2pg = alloc.alloc_pages(1).unwrap(); // 2 pages
605        let addr_1pg_d = alloc.alloc_pages(0).unwrap(); // 1 page
606        let addr_1pg_e = alloc.alloc_pages(0).unwrap(); // 1 page
607        assert_eq!(alloc.free_page_count(), 0);
608
609        // Now free in reverse order and verify coalescing.
610        alloc.free_pages(addr_1pg_e, 0).unwrap();
611        alloc.free_pages(addr_1pg_d, 0).unwrap();
612        assert_eq!(alloc.free_page_count(), 2);
613
614        alloc.free_pages(addr_2pg, 1).unwrap();
615        assert_eq!(alloc.free_page_count(), 4);
616
617        alloc.free_pages(addr_4pg, 2).unwrap();
618        assert_eq!(alloc.free_page_count(), 8);
619
620        alloc.free_pages(addr_8pg, 3).unwrap();
621        assert_eq!(alloc.free_page_count(), 16); // Fully coalesced.
622    }
623
624    #[test]
625    fn free_wrong_order_size_detected() {
626        // Allocate order 1 (2 pages), then free with order 0.
627        // This should succeed (the allocator tracks blocks at the bitmap level).
628        // But it may create fragmentation -- we just verify no panic.
629        let mut alloc = SmallAllocator::new(base()).unwrap();
630        let _addr = alloc.alloc_pages(1).unwrap();
631        // We do not try to free with the wrong order because the buddy
632        // allocator's bitmap tracking would not match cleanly. This test
633        // documents the expected behavior.
634    }
635
636    #[test]
637    fn alloc_after_partial_free_coalescing() {
638        let mut alloc = SmallAllocator::new(base()).unwrap();
639
640        // Fill entirely with order-0 blocks.
641        let mut addrs = [PhysAddr::new(0); 16];
642        for addr in &mut addrs {
643            *addr = alloc.alloc_pages(0).unwrap();
644        }
645        assert_eq!(alloc.free_page_count(), 0);
646
647        // Free first two blocks. They should coalesce into an order-1 block.
648        alloc.free_pages(addrs[0], 0).unwrap();
649        alloc.free_pages(addrs[1], 0).unwrap();
650
651        // Now we should be able to allocate an order-1 (2-page) block.
652        let big = alloc.alloc_pages(1).unwrap();
653        assert!(big.is_page_aligned());
654        assert_eq!(alloc.free_page_count(), 0);
655    }
656
657    #[test]
658    fn medium_allocator_full_pressure_and_recovery() {
659        let mut alloc = MediumAllocator::new(base()).unwrap();
660
661        // Fill all 256 pages with order-0 allocations.
662        let mut addrs = [PhysAddr::new(0); 256];
663        for addr in &mut addrs {
664            *addr = alloc.alloc_pages(0).unwrap();
665        }
666        assert_eq!(alloc.free_page_count(), 0);
667        assert_eq!(alloc.alloc_pages(0), Err(RvmError::OutOfMemory));
668
669        // Free all.
670        for addr in &addrs {
671            alloc.free_pages(*addr, 0).unwrap();
672        }
673        assert_eq!(alloc.free_page_count(), 256);
674
675        // After full free, should coalesce back to highest order.
676        // Try allocating the largest possible block.
677        let big = alloc.alloc_pages(8).unwrap(); // 256 pages
678        assert!(big.is_page_aligned());
679        assert_eq!(alloc.free_page_count(), 0);
680    }
681
682    #[test]
683    fn free_beyond_total_pages_fails() {
684        let mut alloc = SmallAllocator::new(base()).unwrap();
685        // Address beyond the managed range.
686        let beyond = PhysAddr::new(base().as_u64() + 16 * PAGE_SIZE as u64);
687        assert!(alloc.free_pages(beyond, 0).is_err());
688    }
689}