Skip to main content

buddy_slab_allocator/buddy/
mod.rs

1//! Buddy page allocator — page-metadata-based with intrusive free lists.
2//!
3//! The allocator manages one or more contiguous virtual address ranges ("sections").
4//! Each section stores its own [`BuddySection`] descriptor and [`PageMeta`] array
5//! in the caller-provided region prefix, enabling O(1) free-list operations
6//! without any dynamic allocation.
7
8pub mod page_meta;
9
10pub use page_meta::{PFN_NONE, PageFlags, PageMeta};
11
12use core::ptr;
13
14use crate::error::{AllocError, AllocResult};
15use crate::{OsImpl, align_up, is_aligned};
16use page_meta::{free_list_pop, free_list_push, free_list_remove};
17
18/// Maximum buddy order. With 4 KiB pages this gives 2^20 × 4 KiB = 4 GiB blocks.
19pub const MAX_ORDER: usize = 20;
20
21/// DMA32 zone upper bound (4 GiB physical).
22const DMA32_LIMIT: usize = 0x1_0000_0000;
23
24struct RegionLayout {
25    section_start: usize,
26    meta_start: usize,
27    managed_heap_start: usize,
28    managed_heap_size: usize,
29}
30
31pub(crate) struct SectionInitSpec {
32    pub(crate) region_start: usize,
33    pub(crate) region_size: usize,
34    pub(crate) section_ptr: *mut BuddySection,
35    pub(crate) meta_ptr: *mut u8,
36    pub(crate) meta_size: usize,
37    pub(crate) heap_start: usize,
38    pub(crate) heap_size: usize,
39}
40
41/// Public read-only summary of a managed section.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct ManagedSection {
44    pub start: usize,
45    pub size: usize,
46    pub free_pages: usize,
47    pub total_pages: usize,
48}
49
50/// Per-region buddy state stored in the region prefix.
51#[repr(C)]
52pub(crate) struct BuddySection {
53    pub(crate) next: *mut BuddySection,
54    pub(crate) region_start: usize,
55    pub(crate) region_size: usize,
56    pub(crate) meta: *mut PageMeta,
57    pub(crate) max_pages: usize,
58    pub(crate) heap_start: usize,
59    pub(crate) heap_size: usize,
60    pub(crate) free_lists: [u32; MAX_ORDER + 1],
61    pub(crate) free_pages: usize,
62    pub(crate) total_pages: usize,
63}
64
65impl BuddySection {
66    const fn metadata_align() -> usize {
67        let section_align = core::mem::align_of::<BuddySection>();
68        let meta_align = core::mem::align_of::<PageMeta>();
69        if section_align > meta_align {
70            section_align
71        } else {
72            meta_align
73        }
74    }
75
76    fn metadata_layout_for_pages(pages: usize) -> Option<(usize, usize)> {
77        let meta_offset = align_up(
78            core::mem::size_of::<BuddySection>(),
79            core::mem::align_of::<PageMeta>(),
80        );
81        let page_meta_size = pages.checked_mul(core::mem::size_of::<PageMeta>())?;
82        let meta_size = meta_offset.checked_add(page_meta_size)?;
83        Some((meta_offset, meta_size))
84    }
85
86    fn available_heap_pages<const PAGE_SIZE: usize>(
87        region_end: usize,
88        section_start: usize,
89        meta_size: usize,
90    ) -> Option<usize> {
91        let managed_heap_start = align_up(section_start.checked_add(meta_size)?, PAGE_SIZE);
92        if managed_heap_start > region_end {
93            return Some(0);
94        }
95        Some((region_end - managed_heap_start) / PAGE_SIZE)
96    }
97
98    fn can_manage_pages<const PAGE_SIZE: usize>(
99        region_end: usize,
100        section_start: usize,
101        pages: usize,
102    ) -> bool {
103        let Some((_, meta_size)) = Self::metadata_layout_for_pages(pages) else {
104            return false;
105        };
106        let Some(available_pages) =
107            Self::available_heap_pages::<PAGE_SIZE>(region_end, section_start, meta_size)
108        else {
109            return false;
110        };
111        available_pages >= pages
112    }
113
114    fn compute_region_layout<const PAGE_SIZE: usize>(
115        region_start: usize,
116        region_size: usize,
117    ) -> Option<RegionLayout> {
118        if region_size == 0 || !PAGE_SIZE.is_power_of_two() {
119            return None;
120        }
121
122        let region_end = region_start.checked_add(region_size)?;
123        let section_start = align_up(region_start, Self::metadata_align());
124        if section_start >= region_end {
125            return None;
126        }
127
128        let heap_search_start = align_up(
129            section_start.checked_add(core::mem::size_of::<BuddySection>())?,
130            PAGE_SIZE,
131        );
132        let max_pages = if heap_search_start >= region_end {
133            0
134        } else {
135            (region_end - heap_search_start) / PAGE_SIZE
136        };
137
138        let mut low = 0usize;
139        let mut high = max_pages;
140        while low < high {
141            let mid = low + (high - low).div_ceil(2);
142            if Self::can_manage_pages::<PAGE_SIZE>(region_end, section_start, mid) {
143                low = mid;
144            } else {
145                high = mid - 1;
146            }
147        }
148
149        if low == 0 {
150            return None;
151        }
152
153        let (meta_offset, meta_size) = Self::metadata_layout_for_pages(low)?;
154        let meta_start = section_start.checked_add(meta_offset)?;
155        let managed_heap_start = align_up(section_start.checked_add(meta_size)?, PAGE_SIZE);
156        let managed_heap_size = low.checked_mul(PAGE_SIZE)?;
157
158        Some(RegionLayout {
159            section_start,
160            meta_start,
161            managed_heap_start,
162            managed_heap_size,
163        })
164    }
165
166    unsafe fn init_at<const PAGE_SIZE: usize>(
167        section_ptr: *mut BuddySection,
168        region_start: usize,
169        region_size: usize,
170        meta_ptr: *mut u8,
171        meta_size: usize,
172        heap_start: usize,
173        heap_size: usize,
174    ) -> AllocResult {
175        unsafe {
176            if !PAGE_SIZE.is_power_of_two() {
177                return Err(AllocError::InvalidParam);
178            }
179            if !is_aligned(heap_start, PAGE_SIZE) || heap_size == 0 {
180                return Err(AllocError::InvalidParam);
181            }
182
183            let total_pages = heap_size / PAGE_SIZE;
184            let required = BuddyAllocator::<PAGE_SIZE>::required_meta_size(heap_size);
185            if meta_size < required {
186                return Err(AllocError::InvalidParam);
187            }
188
189            let meta = meta_ptr as *mut PageMeta;
190            for i in 0..total_pages {
191                meta.add(i).write(PageMeta::new());
192            }
193
194            section_ptr.write(BuddySection {
195                next: ptr::null_mut(),
196                region_start,
197                region_size,
198                meta,
199                max_pages: total_pages,
200                heap_start,
201                heap_size,
202                free_lists: [PFN_NONE; MAX_ORDER + 1],
203                free_pages: 0,
204                total_pages,
205            });
206
207            let section = &mut *section_ptr;
208            let mut pfn: usize = 0;
209            while pfn < total_pages {
210                let mut order = MAX_ORDER;
211                loop {
212                    let block_pages = 1usize << order;
213                    if block_pages <= total_pages - pfn && (pfn & (block_pages - 1)) == 0 {
214                        break;
215                    }
216                    if order == 0 {
217                        break;
218                    }
219                    order -= 1;
220                }
221                let block_pages = 1usize << order;
222                let m = &mut *section.meta.add(pfn);
223                m.flags = PageFlags::Free;
224                m.order = order as u8;
225                free_list_push(section.meta, &mut section.free_lists, pfn as u32, order);
226                section.free_pages += block_pages;
227                pfn += block_pages;
228            }
229
230            Ok(())
231        }
232    }
233
234    #[inline]
235    fn contains_heap_addr(&self, addr: usize) -> bool {
236        addr >= self.heap_start && addr < self.heap_start + self.heap_size
237    }
238
239    #[inline]
240    fn summary(&self) -> ManagedSection {
241        ManagedSection {
242            start: self.heap_start,
243            size: self.heap_size,
244            free_pages: self.free_pages,
245            total_pages: self.total_pages,
246        }
247    }
248}
249
250/// Page-metadata-based buddy allocator.
251///
252/// `PAGE_SIZE` must be a power of two (commonly 0x1000 = 4 KiB).
253pub struct BuddyAllocator<const PAGE_SIZE: usize = 0x1000> {
254    sections_head: *mut BuddySection,
255    sections_tail: *mut BuddySection,
256    section_count: usize,
257    os: Option<&'static dyn OsImpl>,
258}
259
260// SAFETY: The allocator is designed to be wrapped in a SpinMutex.
261// All section pointers point into caller-provided regions whose lifetime is managed externally.
262unsafe impl<const PAGE_SIZE: usize> Send for BuddyAllocator<PAGE_SIZE> {}
263
264impl<const PAGE_SIZE: usize> BuddyAllocator<PAGE_SIZE> {
265    /// Calculate the metadata-region size (in bytes) required for `heap_size` bytes.
266    pub const fn required_meta_size(heap_size: usize) -> usize {
267        let pages = heap_size / PAGE_SIZE;
268        pages * core::mem::size_of::<PageMeta>()
269    }
270
271    /// Create an uninitialised allocator. Call [`init`](Self::init) before use.
272    pub const fn new() -> Self {
273        Self {
274            sections_head: ptr::null_mut(),
275            sections_tail: ptr::null_mut(),
276            section_count: 0,
277            os: None,
278        }
279    }
280}
281
282impl<const PAGE_SIZE: usize> Default for BuddyAllocator<PAGE_SIZE> {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288impl<const PAGE_SIZE: usize> BuddyAllocator<PAGE_SIZE> {
289    pub(crate) fn reset(&mut self, os: Option<&'static dyn OsImpl>) {
290        self.sections_head = ptr::null_mut();
291        self.sections_tail = ptr::null_mut();
292        self.section_count = 0;
293        self.os = os;
294    }
295
296    /// Initialise the allocator over the first section.
297    ///
298    /// # Safety
299    /// - `region` must be writable and remain valid for the lifetime of this allocator.
300    /// - Bytes consumed by metadata become unavailable for allocation.
301    pub unsafe fn init(
302        &mut self,
303        region: &mut [u8],
304        os: Option<&'static dyn OsImpl>,
305    ) -> AllocResult {
306        unsafe {
307            self.reset(os);
308            self.add_region(region)
309        }
310    }
311
312    /// Add a new managed region after initialisation.
313    ///
314    /// # Safety
315    /// - `region` must be writable and remain valid for the lifetime of this allocator.
316    /// - The region must not overlap any existing managed region.
317    pub unsafe fn add_region(&mut self, region: &mut [u8]) -> AllocResult {
318        unsafe {
319            let region_start = region.as_mut_ptr() as usize;
320            let region_size = region.len();
321            let layout =
322                BuddySection::compute_region_layout::<PAGE_SIZE>(region_start, region_size)
323                    .ok_or(AllocError::InvalidParam)?;
324            self.add_region_raw(SectionInitSpec {
325                region_start,
326                region_size,
327                section_ptr: layout.section_start as *mut BuddySection,
328                meta_ptr: layout.meta_start as *mut u8,
329                meta_size: Self::required_meta_size(layout.managed_heap_size),
330                heap_start: layout.managed_heap_start,
331                heap_size: layout.managed_heap_size,
332            })
333        }
334    }
335
336    pub(crate) unsafe fn add_region_raw(&mut self, spec: SectionInitSpec) -> AllocResult {
337        unsafe {
338            let region_size = spec.region_size;
339            let region_end = spec
340                .region_start
341                .checked_add(region_size)
342                .ok_or(AllocError::InvalidParam)?;
343            let heap_end = spec
344                .heap_start
345                .checked_add(spec.heap_size)
346                .ok_or(AllocError::InvalidParam)?;
347            if heap_end > region_end {
348                return Err(AllocError::InvalidParam);
349            }
350
351            let mut section = self.sections_head;
352            while !section.is_null() {
353                let existing = &*section;
354                let existing_end = existing
355                    .region_start
356                    .checked_add(existing.region_size)
357                    .ok_or(AllocError::InvalidParam)?;
358                if spec.region_start < existing_end && existing.region_start < region_end {
359                    return Err(AllocError::MemoryOverlap);
360                }
361                section = existing.next;
362            }
363
364            BuddySection::init_at::<PAGE_SIZE>(
365                spec.section_ptr,
366                spec.region_start,
367                spec.region_size,
368                spec.meta_ptr,
369                spec.meta_size,
370                spec.heap_start,
371                spec.heap_size,
372            )?;
373
374            if self.sections_head.is_null() {
375                self.sections_head = spec.section_ptr;
376            } else {
377                (*self.sections_tail).next = spec.section_ptr;
378            }
379            self.sections_tail = spec.section_ptr;
380            self.section_count += 1;
381
382            log::debug!(
383                "BuddyAllocator: add section region {:#x}+{:#x}, heap {:#x}..{:#x}, {} pages",
384                spec.region_start,
385                spec.region_size,
386                spec.heap_start,
387                heap_end,
388                spec.heap_size / PAGE_SIZE,
389            );
390
391            Ok(())
392        }
393    }
394
395    /// Number of managed sections.
396    pub fn section_count(&self) -> usize {
397        self.section_count
398    }
399
400    /// Read-only summary for a managed section by registration order.
401    pub fn section(&self, index: usize) -> Option<ManagedSection> {
402        let mut current = self.sections_head;
403        let mut i = 0usize;
404        while !current.is_null() {
405            if i == index {
406                return Some(unsafe { (&*current).summary() });
407            }
408            current = unsafe { (*current).next };
409            i += 1;
410        }
411        None
412    }
413
414    /// Total number of pages managed across all sections.
415    pub fn total_pages(&self) -> usize {
416        let mut total = 0usize;
417        let mut current = self.sections_head;
418        while !current.is_null() {
419            total += unsafe { (*current).total_pages };
420            current = unsafe { (*current).next };
421        }
422        total
423    }
424
425    /// Total managed heap bytes across all sections.
426    ///
427    /// This counts only bytes in allocatable heaps, excluding region-prefix metadata.
428    pub fn managed_bytes(&self) -> usize {
429        let mut total = 0usize;
430        let mut current = self.sections_head;
431        while !current.is_null() {
432            total += unsafe { (*current).heap_size };
433            current = unsafe { (*current).next };
434        }
435        total
436    }
437
438    /// Number of currently free pages across all sections.
439    pub fn free_pages(&self) -> usize {
440        let mut total = 0usize;
441        let mut current = self.sections_head;
442        while !current.is_null() {
443            total += unsafe { (*current).free_pages };
444            current = unsafe { (*current).next };
445        }
446        total
447    }
448
449    /// Allocated backend bytes across all sections.
450    ///
451    /// This is computed as managed heap bytes minus currently free page bytes.
452    /// It reflects page-level occupancy, so it includes slab pages, alignment
453    /// amplification, and internal fragmentation.
454    pub fn allocated_bytes(&self) -> usize {
455        self.managed_bytes()
456            .saturating_sub(self.free_pages().saturating_mul(PAGE_SIZE))
457    }
458
459    /// Allocate `count` contiguous pages, returning the virtual address.
460    pub fn alloc_pages(&mut self, count: usize, align: usize) -> AllocResult<usize> {
461        if count == 0 {
462            return Err(AllocError::InvalidParam);
463        }
464        let align = if align == 0 { PAGE_SIZE } else { align };
465        if !align.is_power_of_two() || align < PAGE_SIZE {
466            return Err(AllocError::InvalidParam);
467        }
468
469        let order = count.next_power_of_two().trailing_zeros() as usize;
470        if order > MAX_ORDER {
471            return Err(AllocError::InvalidParam);
472        }
473
474        let align_order = (align / PAGE_SIZE).trailing_zeros() as usize;
475        let effective_order = order.max(align_order);
476
477        let mut section = self.sections_head;
478        while !section.is_null() {
479            if let Ok(addr) = unsafe { Self::alloc_from_section(&mut *section, effective_order) } {
480                return Ok(addr);
481            }
482            section = unsafe { (*section).next };
483        }
484
485        Err(AllocError::NoMemory)
486    }
487
488    fn alloc_from_section(section: &mut BuddySection, order: usize) -> AllocResult<usize> {
489        let mut found_order = order;
490        while found_order <= MAX_ORDER {
491            if section.free_lists[found_order] != PFN_NONE {
492                break;
493            }
494            found_order += 1;
495        }
496        if found_order > MAX_ORDER {
497            return Err(AllocError::NoMemory);
498        }
499
500        let pfn = unsafe { free_list_pop(section.meta, &mut section.free_lists, found_order) };
501        debug_assert_ne!(pfn, PFN_NONE);
502
503        let mut current_order = found_order;
504        while current_order > order {
505            current_order -= 1;
506            let buddy_pfn = pfn as usize + (1 << current_order);
507            unsafe {
508                let bm = &mut *section.meta.add(buddy_pfn);
509                bm.flags = PageFlags::Free;
510                bm.order = current_order as u8;
511                free_list_push(
512                    section.meta,
513                    &mut section.free_lists,
514                    buddy_pfn as u32,
515                    current_order,
516                );
517            }
518        }
519
520        unsafe {
521            let m = &mut *section.meta.add(pfn as usize);
522            m.flags = PageFlags::Allocated;
523            m.order = order as u8;
524        }
525
526        section.free_pages -= 1 << order;
527        Ok(section.heap_start + (pfn as usize) * PAGE_SIZE)
528    }
529
530    /// Allocate pages whose *physical* address is below 4 GiB (DMA32 zone).
531    pub fn alloc_pages_lowmem(&mut self, count: usize, align: usize) -> AllocResult<usize> {
532        let os = self.os.ok_or(AllocError::InvalidParam)?;
533
534        if count == 0 {
535            return Err(AllocError::InvalidParam);
536        }
537        let align = if align == 0 { PAGE_SIZE } else { align };
538        if !align.is_power_of_two() || align < PAGE_SIZE {
539            return Err(AllocError::InvalidParam);
540        }
541
542        let order = count.next_power_of_two().trailing_zeros() as usize;
543        let align_order = (align / PAGE_SIZE).trailing_zeros() as usize;
544        let effective_order = order.max(align_order);
545        if effective_order > MAX_ORDER {
546            return Err(AllocError::InvalidParam);
547        }
548
549        let mut section = self.sections_head;
550        while !section.is_null() {
551            if let Ok(addr) =
552                unsafe { Self::alloc_lowmem_from_section(&mut *section, effective_order, os) }
553            {
554                return Ok(addr);
555            }
556            section = unsafe { (*section).next };
557        }
558
559        Err(AllocError::NoMemory)
560    }
561
562    fn alloc_lowmem_from_section(
563        section: &mut BuddySection,
564        effective_order: usize,
565        os: &'static dyn OsImpl,
566    ) -> AllocResult<usize> {
567        for search_order in effective_order..=MAX_ORDER {
568            let mut pfn_u32 = section.free_lists[search_order];
569            while pfn_u32 != PFN_NONE {
570                let addr = section.heap_start + (pfn_u32 as usize) * PAGE_SIZE;
571                let phys = os.virt_to_phys(addr);
572                let block_bytes = (1usize << search_order) * PAGE_SIZE;
573                if phys + block_bytes <= DMA32_LIMIT {
574                    unsafe {
575                        free_list_remove(
576                            section.meta,
577                            &mut section.free_lists,
578                            pfn_u32,
579                            search_order,
580                        );
581                    }
582
583                    let mut current_order = search_order;
584                    while current_order > effective_order {
585                        current_order -= 1;
586                        let buddy_pfn = pfn_u32 as usize + (1 << current_order);
587                        unsafe {
588                            let bm = &mut *section.meta.add(buddy_pfn);
589                            bm.flags = PageFlags::Free;
590                            bm.order = current_order as u8;
591                            free_list_push(
592                                section.meta,
593                                &mut section.free_lists,
594                                buddy_pfn as u32,
595                                current_order,
596                            );
597                        }
598                    }
599
600                    unsafe {
601                        let m = &mut *section.meta.add(pfn_u32 as usize);
602                        m.flags = PageFlags::Allocated;
603                        m.order = effective_order as u8;
604                    }
605                    section.free_pages -= 1 << effective_order;
606                    return Ok(addr);
607                }
608                pfn_u32 = unsafe { (*section.meta.add(pfn_u32 as usize)).next };
609            }
610        }
611
612        Err(AllocError::NoMemory)
613    }
614
615    /// Free pages previously obtained via [`alloc_pages`](Self::alloc_pages).
616    ///
617    /// `addr` must be the exact address returned by alloc. The allocator frees
618    /// the full block size recorded in page metadata, which may be larger than
619    /// `count` if the original allocation was rounded up for buddy order or alignment.
620    pub fn dealloc_pages(&mut self, addr: usize, count: usize) {
621        let Some(section) = self.find_section_by_addr_mut(addr) else {
622            debug_assert!(
623                false,
624                "dealloc_pages called with address outside all sections"
625            );
626            return;
627        };
628
629        debug_assert!(is_aligned(addr, PAGE_SIZE));
630        debug_assert!(count > 0);
631
632        let pfn = (addr - section.heap_start) / PAGE_SIZE;
633        debug_assert!(pfn < section.max_pages);
634        let stored = unsafe { &*section.meta.add(pfn) };
635        debug_assert!(
636            stored.flags == PageFlags::Allocated || stored.flags == PageFlags::Slab,
637            "dealloc_pages called on non-allocated block"
638        );
639
640        let expected_order = count.next_power_of_two().trailing_zeros() as usize;
641        let order = stored.order as usize;
642        debug_assert!(
643            expected_order <= order,
644            "dealloc_pages count implies larger order than the allocated block"
645        );
646        Self::dealloc_in_section(section, pfn, order);
647    }
648
649    /// Mark the page at `addr` with the given flags (used by slab to tag pages).
650    ///
651    /// # Safety
652    /// The caller must ensure `addr` is valid and properly allocated.
653    pub unsafe fn set_page_flags(&mut self, addr: usize, flags: PageFlags) -> AllocResult {
654        unsafe {
655            let section = self
656                .find_section_by_addr_mut(addr)
657                .ok_or(AllocError::NotFound)?;
658            let pfn = (addr - section.heap_start) / PAGE_SIZE;
659            (*section.meta.add(pfn)).flags = flags;
660            Ok(())
661        }
662    }
663
664    /// Read the flags of the page containing `addr`.
665    pub fn page_flags(&self, addr: usize) -> AllocResult<PageFlags> {
666        let section = self
667            .find_section_by_addr(addr)
668            .ok_or(AllocError::NotFound)?;
669        let pfn = (addr - section.heap_start) / PAGE_SIZE;
670        Ok(unsafe { (*section.meta.add(pfn)).flags })
671    }
672
673    fn dealloc_in_section(section: &mut BuddySection, mut pfn: usize, mut order: usize) {
674        let freed_pages = 1usize << order;
675
676        while order < MAX_ORDER {
677            let buddy_pfn = pfn ^ (1 << order);
678            if buddy_pfn >= section.max_pages {
679                break;
680            }
681            let buddy = unsafe { &*section.meta.add(buddy_pfn) };
682            if buddy.flags != PageFlags::Free || buddy.order as usize != order {
683                break;
684            }
685            unsafe {
686                free_list_remove(
687                    section.meta,
688                    &mut section.free_lists,
689                    buddy_pfn as u32,
690                    order,
691                );
692            }
693            pfn = pfn.min(buddy_pfn);
694            order += 1;
695        }
696
697        unsafe {
698            let m = &mut *section.meta.add(pfn);
699            m.flags = PageFlags::Free;
700            m.order = order as u8;
701            free_list_push(section.meta, &mut section.free_lists, pfn as u32, order);
702        }
703        section.free_pages += freed_pages;
704    }
705
706    fn find_section_by_addr(&self, addr: usize) -> Option<&BuddySection> {
707        let mut section = self.sections_head;
708        while !section.is_null() {
709            let current = unsafe { &*section };
710            if current.contains_heap_addr(addr) {
711                return Some(current);
712            }
713            section = current.next;
714        }
715        None
716    }
717
718    fn find_section_by_addr_mut(&mut self, addr: usize) -> Option<&mut BuddySection> {
719        let mut section = self.sections_head;
720        while !section.is_null() {
721            let current = unsafe { &mut *section };
722            if current.contains_heap_addr(addr) {
723                return Some(current);
724            }
725            section = current.next;
726        }
727        None
728    }
729}