| // Copyright 2021 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| mod virtual_alloc_ffi; |
| |
| use crate::kernel::align::{is_rounded, roundup}; |
| use crate::kernel::types::PAddr; |
| use crate::vm::arch_vm_aspace::{ |
| ARCH_MMU_FLAG_CACHED, ARCH_MMU_FLAG_PERM_READ, ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_UNMAP_OPTION_NONE, ArchMmuFlags, ExistingEntryAction, |
| }; |
| use crate::vm::page_state::VmPageState; |
| use crate::vm::pmm; |
| use crate::vm::vm_aspace::VmAspace; |
| use bitmap::{Bitmap, RawBitmapGeneric}; |
| use bitmap_storage::BitmapStorage; |
| use debug::ltracef; |
| use fbl::Canary; |
| use page_list::{OwnedPage, PageList}; |
| use pin_init::stack_pin_init; |
| use zx_status::Status; |
| |
| const LOCAL_TRACE: u32 = 0; |
| |
| mod page_list { |
| use crate::kernel::types::PAddr; |
| use crate::vm::page::{VmPage, VmPageDoublyLinkedList, VmPagePtr}; |
| use crate::vm::page_state::VmPageState; |
| use crate::vm::pmm; |
| use core::ops::Deref; |
| use core::pin::Pin; |
| use core::ptr::NonNull; |
| use pin_init::{PinInit, pin_data, pin_init, pinned_drop}; |
| use zx_status::Status; |
| |
| /// An exclusively owned, unmapped physical page allocated from the PMM that is not currently |
| /// in any intrusive list. Frees the page back to the PMM on drop unless transferred into a |
| /// [`PageList`]. |
| pub struct OwnedPage(NonNull<VmPage>); |
| |
| impl OwnedPage { |
| /// Allocates a single physical page from the PMM. |
| #[cfg(ktest)] |
| pub fn alloc(flags: u32) -> Result<(Self, PAddr), Status> { |
| let (page, paddr) = pmm::alloc_page(flags)?; |
| Ok((Self(page.as_non_null()), paddr)) |
| } |
| |
| /// Wraps an allocated PMM page that is about to be unmapped and freed. |
| /// |
| /// # Safety |
| /// |
| /// `page` must be a valid allocated PMM page uniquely owned by the caller, not in any |
| /// intrusive list, and must be unmapped before this `OwnedPage` (or any [`PageList`] it is |
| /// placed into) is dropped. |
| pub unsafe fn from_allocated(page: VmPagePtr) -> Self { |
| Self(page.as_non_null()) |
| } |
| |
| fn into_raw(self) -> NonNull<VmPage> { |
| let ptr = self.0; |
| core::mem::forget(self); |
| ptr |
| } |
| } |
| |
| impl Drop for OwnedPage { |
| fn drop(&mut self) { |
| // SAFETY: `self.0` is an exclusively owned, unmapped PMM page not in any list. |
| unsafe { |
| pmm::free_page(VmPagePtr::new(self.0)); |
| } |
| } |
| } |
| |
| /// An RAII wrapper around [`VmPageDoublyLinkedList`] that holds exclusive conceptual ownership |
| /// of its pages and frees any remaining pages back to the PMM on drop. |
| #[pin_data(PinnedDrop)] |
| pub struct PageList { |
| #[pin] |
| list: VmPageDoublyLinkedList, |
| } |
| |
| impl PageList { |
| pub fn new() -> impl PinInit<Self, core::convert::Infallible> { |
| pin_init!(Self { |
| list <- VmPageDoublyLinkedList::new(), |
| }) |
| } |
| |
| /// Allocates `count` pages from the PMM and appends them to `self`. |
| pub fn alloc_pages(self: Pin<&mut Self>, count: usize, flags: u32) -> Result<(), Status> { |
| pmm::alloc_pages(count, flags, self.project().list) |
| } |
| |
| /// Allocates `count` contiguous pages from the PMM and appends them to `self`. |
| pub fn alloc_contiguous( |
| self: Pin<&mut Self>, |
| count: usize, |
| flags: u32, |
| align_log2: u8, |
| ) -> Result<PAddr, Status> { |
| pmm::alloc_contiguous(count, flags, align_log2, self.project().list) |
| } |
| |
| /// Sets the page state for all pages currently in `self`. |
| pub fn set_state(self: Pin<&mut Self>, state: VmPageState) { |
| for p in self.list.iter() { |
| // SAFETY: `self` holds exclusive conceptual ownership of all pages in the list. |
| unsafe { |
| p.set_state(state); |
| } |
| } |
| } |
| |
| /// Appends `page` to the back of `self`. |
| pub fn push_back(self: Pin<&mut Self>, page: OwnedPage) { |
| // SAFETY: `page` is a valid PMM page not in any container, and ownership is |
| // transferred to `self`. |
| unsafe { |
| self.project().list.push_back_raw(page.into_raw()); |
| } |
| } |
| |
| /// Prepends `page` to the front of `self`. |
| #[cfg(ktest)] |
| pub fn push_front(self: Pin<&mut Self>, page: OwnedPage) { |
| // SAFETY: `page` is a valid PMM page not in any container, and ownership is |
| // transferred to `self`. |
| unsafe { |
| self.project().list.push_front_raw(page.into_raw()); |
| } |
| } |
| |
| /// Removes and returns the first page in `self`. |
| #[cfg(ktest)] |
| pub fn pop_front(self: Pin<&mut Self>) -> Option<OwnedPage> { |
| self.project().list.pop_front().map(OwnedPage) |
| } |
| |
| /// Removes and returns the last page in `self`. |
| #[cfg(ktest)] |
| pub fn pop_back(self: Pin<&mut Self>) -> Option<OwnedPage> { |
| self.project().list.pop_back().map(OwnedPage) |
| } |
| |
| /// Splices all pages from `other` onto the front of `self`. |
| pub fn splice_front(self: Pin<&mut Self>, other: Pin<&mut Self>) { |
| self.project().list.cursor_front_mut().splice(other.project().list); |
| } |
| |
| /// Splices all pages from `other` onto the back of `self` and returns a [`PageCursor`] |
| /// positioned at the first newly spliced page (or at the end if `other` was empty). |
| pub fn splice_back(mut self: Pin<&mut Self>, other: Pin<&mut Self>) -> PageCursor<'_> { |
| let current = other.list.front().map(NonNull::from); |
| self.as_mut().project().list.splice(other.project().list); |
| PageCursor { list: self, current } |
| } |
| |
| /// Frees all pages currently in `self` back to the PMM, leaving `self` empty. |
| pub fn free_pages(self: Pin<&mut Self>) { |
| if !self.list.is_empty() { |
| // SAFETY: All pages in `self` are valid allocated PMM pages owned by `self`. |
| unsafe { |
| pmm::free_list(self.project().list); |
| } |
| } |
| } |
| |
| /// Clears `self` without freeing the pages back to the PMM, transferring conceptual |
| /// ownership of the pages to the hardware page tables after mapping succeeds. |
| pub fn forget_pages(self: Pin<&mut Self>) { |
| self.project().list.clear(); |
| } |
| } |
| |
| impl Deref for PageList { |
| type Target = VmPageDoublyLinkedList; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.list |
| } |
| } |
| |
| #[pinned_drop] |
| impl PinnedDrop for PageList { |
| fn drop(self: Pin<&mut Self>) { |
| self.free_pages(); |
| } |
| } |
| |
| /// A cursor over a [`PageList`] that holds exclusive mutable access to the list. |
| pub struct PageCursor<'a> { |
| list: Pin<&'a mut PageList>, |
| current: Option<NonNull<VmPage>>, |
| } |
| |
| impl PageCursor<'_> { |
| /// Returns whether the cursor currently points to a valid page. |
| pub fn is_valid(&self) -> bool { |
| self.current.is_some() |
| } |
| |
| /// Sets the state of the page at the current cursor position. |
| /// |
| /// # Panics |
| /// |
| /// Panics if the cursor is at the end of the list. |
| pub fn set_state(&mut self, state: VmPageState) { |
| let page = self.current.expect("cursor must be valid"); |
| // SAFETY: `self.list` is exclusively borrowed for `'a`, and `self.current` is a |
| // valid element of `self.list`, which holds exclusive conceptual ownership of all |
| // pages in the list. |
| unsafe { |
| page.as_ref().set_state(state); |
| } |
| } |
| |
| /// Returns the physical address of the page at the current cursor position. |
| /// |
| /// # Panics |
| /// |
| /// Panics if the cursor is at the end of the list. |
| pub fn paddr(&self) -> PAddr { |
| let page = self.current.expect("cursor must be valid"); |
| // SAFETY: `self.current` is a valid element of `self.list`. |
| unsafe { page.as_ref().paddr() } |
| } |
| |
| /// Advances the cursor to the next page in the list. |
| pub fn move_next(&mut self) { |
| if let Some(page) = self.current { |
| // SAFETY: `page` is a valid member of `self.list`. |
| let mut cursor = |
| unsafe { self.list.as_mut().project().list.cursor_at(page.as_ref()) }; |
| cursor.move_next(); |
| self.current = cursor.get().map(NonNull::from); |
| } |
| } |
| } |
| } |
| |
| mod bitmap_storage { |
| use bitmap::Storage; |
| use core::ptr::NonNull; |
| use zx_status::Status; |
| |
| // Custom storage for the bitmap class that provides for a statically sized storage that can be |
| // initialized after construction. |
| #[derive(Debug, Default)] |
| pub struct BitmapStorage { |
| base: Option<NonNull<usize>>, |
| size: usize, |
| } |
| |
| impl BitmapStorage { |
| pub const fn new() -> Self { |
| Self { base: None, size: 0 } |
| } |
| |
| /// Initializes the backing storage pointer and size in bytes, zero-initializing the memory. |
| /// |
| /// # Panics |
| /// |
| /// Panics if `base` is zero or not aligned to `usize`. |
| /// |
| /// # Safety |
| /// |
| /// `base` must point to `size` bytes of valid, mapped memory uniquely owned by the |
| /// containing `VirtualAlloc` instance until [`Self::reset`] is called. |
| pub unsafe fn init(&mut self, base: usize, size: usize) { |
| let ptr = NonNull::new(core::ptr::with_exposed_provenance_mut::<usize>(base)) |
| .expect("base must be non-zero"); |
| assert!(ptr.is_aligned(), "base must be aligned to usize"); |
| let num_words = size / core::mem::size_of::<usize>(); |
| // SAFETY: Caller guarantees `[base, base + size)` is mapped and uniquely owned, and |
| // alignment was verified above. Zeroing ensures the memory is initialized before any |
| // slice is formed in `get_data` or `get_data_mut`. |
| unsafe { |
| core::ptr::write_bytes(ptr.as_ptr(), 0, num_words); |
| } |
| self.base = Some(ptr); |
| self.size = size; |
| } |
| |
| pub fn reset(&mut self) { |
| self.base = None; |
| self.size = 0; |
| } |
| |
| pub fn get_size(&self) -> usize { |
| self.size |
| } |
| } |
| |
| impl Storage for BitmapStorage { |
| fn allocate(&mut self, size: usize) -> Result<(), Status> { |
| if size > self.size { |
| return Err(Status::NO_MEMORY); |
| } |
| Ok(()) |
| } |
| |
| fn get_data(&self) -> &[usize] { |
| // SAFETY: When `self.base` is `Some`, it points to `self.size` bytes of mapped, |
| // page-aligned, initialized memory owned by this `VirtualAlloc` instance. When `None`, |
| // `self.size` is 0. |
| unsafe { |
| core::slice::from_raw_parts( |
| self.base.unwrap_or(NonNull::dangling()).as_ptr(), |
| self.size / core::mem::size_of::<usize>(), |
| ) |
| } |
| } |
| |
| fn get_data_mut(&mut self) -> &mut [usize] { |
| // SAFETY: When `self.base` is `Some`, it points to `self.size` bytes of mapped, |
| // page-aligned, initialized memory uniquely owned by this `VirtualAlloc` instance. When |
| // `None`, `self.size` is 0. |
| unsafe { |
| core::slice::from_raw_parts_mut( |
| self.base.unwrap_or(NonNull::dangling()).as_ptr(), |
| self.size / core::mem::size_of::<usize>(), |
| ) |
| } |
| } |
| } |
| } |
| |
| /// `VirtualAlloc` is a page granule allocator that manages a given virtual region and provides |
| /// virtually contiguous allocations inside that region. This allocator explicitly has no |
| /// dependency on the heap and retrieves all its backing memory directly from the pmm. To achieve |
| /// this it maps pages directly into the hardware page tables via the arch aspace, and |
| /// consequently assumes that these operations will only depend on the pmm and not the heap for |
| /// allocating any intermediate page tables. |
| /// |
| /// This class is thread-unsafe. |
| #[repr(C)] |
| pub struct VirtualAlloc { |
| canary: Canary<{ fbl::magic(b"VALC") }>, |
| |
| // Page state to set allocated pages to. |
| allocated_page_state: VmPageState, |
| |
| // Record of that padding to be applied to every allocation. |
| alloc_guard: usize, |
| |
| // The virtual address of the start of the range managed by `bitmap`. |
| alloc_base: usize, |
| |
| // Heuristic used to attempt to begin searching for a free run in `bitmap` at an optimal point. |
| next_search_start: usize, |
| |
| align_log2: usize, |
| |
| // Bitmap representing allocated pages in the virtual range being managed. This is fully |
| // preallocated and reserves for itself a portion of the `init` virtual address range. |
| bitmap: RawBitmapGeneric<BitmapStorage>, |
| } |
| |
| zr::static_assert!(core::mem::size_of::<VirtualAlloc>() == 64); |
| zr::static_assert!(core::mem::align_of::<VirtualAlloc>() == 8); |
| |
| impl Drop for VirtualAlloc { |
| fn drop(&mut self) { |
| self.destroy(); |
| } |
| } |
| |
| impl VirtualAlloc { |
| /// Allocator needs to be told what state to set any allocated pages to. This allows for using |
| /// the allocator as a heap or generic allocator for an object. |
| pub const fn new(allocated_page_state: VmPageState) -> Self { |
| Self { |
| canary: Canary::new(), |
| allocated_page_state, |
| alloc_guard: 0, |
| alloc_base: 0, |
| next_search_start: 0, |
| align_log2: 0, |
| bitmap: RawBitmapGeneric::new(BitmapStorage::new()), |
| } |
| } |
| |
| /// Initialize the allocator and make it ready for use. Takes a virtual address range as a |
| /// base and size in bytes. These both must be page aligned and there must be no pages mapped |
| /// into the hardware page tables for this range. |
| /// `alloc_guard` is the minimum number of virtual pages to place between two allocations and |
| /// serve as guard pages. Any reads or writes that over or under run an allocation into the |
| /// padding will trigger an immediate hardware page fault. |
| pub fn init( |
| &mut self, |
| base: usize, |
| size: usize, |
| alloc_guard: usize, |
| align_log2: usize, |
| ) -> Result<(), Status> { |
| self.canary.assert(); |
| |
| if self.alloc_base != 0 { |
| // This has already been initialized. |
| return Err(Status::BAD_STATE); |
| } |
| |
| if align_log2 < page::SHIFT { |
| return Err(Status::INVALID_ARGS); |
| } |
| self.align_log2 = align_log2; |
| |
| let vaddr_align = 1usize << self.align_log2; |
| |
| if size == 0 |
| || !is_rounded(size, vaddr_align) |
| || !is_rounded(base, vaddr_align) |
| || base.wrapping_add(size) < base |
| { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| // Work how how many pages we need for the bitmap. |
| let total_pages = size / page::SIZE; |
| const BITS_PER_PAGE: usize = page::SIZE * (u8::BITS as usize); |
| let bitmap_pages = roundup(total_pages, BITS_PER_PAGE) / BITS_PER_PAGE; |
| |
| // Validate that there will be anything left after allocating the bitmap for an actual |
| // allocation. A single allocation needs padding on both sides of it. This ignores |
| // alignment problems caused by the bitmap, and so it's still possible for non page size |
| // alignments that if this check passes that no allocations are possible, but this is not |
| // meant to be an exhaustive guard. |
| if bitmap_pages + alloc_guard * 2 >= total_pages { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| // Allocate and map the bitmap pages into the start of the range we were given. |
| self.alloc_map_pages(base, bitmap_pages)?; |
| // SAFETY: `[base, base + bitmap_pages * page::SIZE)` was just mapped by `alloc_map_pages` |
| // and remains uniquely owned and mapped until `destroy`. |
| unsafe { |
| self.bitmap.storage_mut().init(base, bitmap_pages * page::SIZE); |
| } |
| |
| // Initialize the bitmap, reserving its own pages. |
| self.alloc_base = base; |
| let _ = self.bitmap.reset(total_pages); |
| let _ = self.bitmap.set(0, bitmap_pages); |
| |
| // Set our first search to happen after the bitmap. |
| self.next_search_start = bitmap_pages; |
| |
| self.alloc_guard = alloc_guard; |
| Ok(()) |
| } |
| |
| fn bitmap_alloc_range( |
| &mut self, |
| num_pages: usize, |
| start: usize, |
| end: usize, |
| ) -> Result<usize, Status> { |
| debug_assert!(end >= start); |
| debug_assert!(num_pages > 0); |
| let align_pages = 1usize << (self.align_log2 - page::SHIFT); |
| // Want to find a run of `num_pages` + padding on either end. By over-searching we can |
| // ensure there is always `alloc_guard` unused pages / unset-bits between each allocation. |
| let find_pages = num_pages + self.alloc_guard * 2; |
| |
| // Helper to finalize an allocated range once found. This just allows for structuring the |
| // code in a slightly more readable way for the two different kinds of searches. |
| let complete_alloc = |this: &mut Self, mut start_index: usize| -> Result<usize, Status> { |
| // Increase our start to skip the padding we want to leave. |
| start_index += this.alloc_guard; |
| // Record the end of this allocation as our next search start. We set the end to not |
| // include the padding so that the padding at the end of this allocation becomes the |
| // padding at the start of the next one. |
| this.next_search_start = start_index + num_pages; |
| // Set the bits for the 'inner' allocation, leaving the padding we found unset. |
| let _ = this.bitmap.set(start_index, start_index + num_pages); |
| Ok(start_index) |
| }; |
| |
| // If requested less pages than the alignment then do not bother finding an aligned range, |
| // just find anything. The assumption here is that the block of pages we map in later will |
| // not be large enough to benefit from any alignment, so might as well avoid fragmentation |
| // and do a more efficient search. |
| if num_pages >= align_pages && align_pages > 1 { |
| let mut current_start = start; |
| loop { |
| // Construct a candidate range from the start. This candidate needs to be chosen |
| // such that after skipping the allocation padding it is aligned. |
| let mut candidate = roundup(current_start, align_pages); |
| if candidate - current_start < self.alloc_guard { |
| // Add on sufficient alignment multiples that we will be able to subtract |
| // `alloc_guard` without ending up below `start`. |
| candidate += roundup(self.alloc_guard, align_pages); |
| } |
| candidate -= self.alloc_guard; |
| // If the range from the candidate would exceed our search range, then no aligned |
| // range. |
| if candidate + find_pages > end { |
| break; |
| } |
| // Scan from the candidate and see if all the bits are clear. |
| let Some(set_bit) = self.bitmap.scan(candidate, candidate + find_pages, false) |
| else { |
| return complete_alloc(self, candidate); |
| }; |
| // Search from the bit that was set to find the next unset bit. This will become |
| // our next starting search location. |
| let Some(next_start) = self.bitmap.scan(set_bit, end, true) else { |
| // If all the bits are set then no aligned range. |
| break; |
| }; |
| debug_assert!(next_start > current_start); |
| current_start = next_start; |
| } |
| } |
| // See if there's an unaligned range that will satisfy. |
| let alloc_start = self.bitmap.find(false, start, end, find_pages)?; |
| complete_alloc(self, alloc_start) |
| } |
| |
| fn bitmap_alloc(&mut self, num_pages: usize) -> Result<usize, Status> { |
| // First search from our saved recommended starting location. |
| let result = self.bitmap_alloc_range(num_pages, self.next_search_start, self.bitmap.size()); |
| if result.is_err() { |
| // Try again from the beginning (skipping the bitmap itself). Still search to the end |
| // just in case the original search start was in the middle of a free run. |
| return self.bitmap_alloc_range(num_pages, self.bitmap_pages(), self.bitmap.size()); |
| } |
| result |
| } |
| |
| /// `alloc_pages` can only be called after a successful `init` call, otherwise it will return |
| /// an error. Number of pages requested must be non zero. |
| pub fn alloc_pages(&mut self, pages: usize) -> Result<usize, Status> { |
| self.canary.assert(); |
| |
| if self.alloc_base == 0 { |
| return Err(Status::BAD_STATE); |
| } |
| |
| if pages == 0 { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| // Allocate space from the bitmap, it will set the bits and ensure padding is left around |
| // the allocation. |
| let start = self.bitmap_alloc(pages)?; |
| |
| // Turn the bitmap index into a virtual address and allocate the pages there. |
| let vstart = self.alloc_base + start * page::SIZE; |
| if let Err(status) = self.alloc_map_pages(vstart, pages) { |
| // Return the range back to the bitmap. |
| self.bitmap_free(start, pages); |
| return Err(status); |
| } |
| ltracef!("Allocated {pages} pages at {vstart:#x}\n"); |
| Ok(vstart) |
| } |
| |
| fn bitmap_free(&mut self, start: usize, num_pages: usize) { |
| assert!(start >= self.bitmap_pages()); |
| assert!(start < self.bitmap.size()); |
| assert!(num_pages <= self.bitmap.size() - start); |
| debug_assert!(self.bitmap.scan(start, start + num_pages, true).is_none()); |
| |
| let status = self.bitmap.clear(start, start + num_pages); |
| assert!(status.is_ok()); |
| if start < self.next_search_start { |
| self.next_search_start = start; |
| // To attempt to keep allocations compact check `alloc_guard` bits backwards, and move |
| // our search start if unset. This ensures that if we alloc+free that our |
| // `next_search_start` gets reset to the original location, otherwise it will |
| // constantly creep by `alloc_guard`. |
| if self.next_search_start >= self.alloc_guard { |
| if let Some(candidate) = self.bitmap.reverse_scan( |
| self.next_search_start - self.alloc_guard, |
| self.next_search_start, |
| false, |
| ) { |
| ltracef!( |
| "Reverse scan moved search from {} part way to {}\n", |
| self.next_search_start, |
| candidate + 1 |
| ); |
| self.next_search_start = candidate + 1; |
| } else { |
| ltracef!( |
| "Reverse scan moved search from {} all the way to {}\n", |
| self.next_search_start, |
| self.next_search_start - self.alloc_guard |
| ); |
| self.next_search_start -= self.alloc_guard; |
| } |
| } |
| } |
| } |
| |
| /// Returns a non-zero number of pages at the given virtual address. Partial frees are |
| /// supported such that if 2 pages were allocated at X it is allowed to `free_pages(X, 1)` and |
| /// separately `free_pages(X + page::SIZE, 1)`. |
| pub fn free_pages(&mut self, vaddr: usize, pages: usize) { |
| assert!(self.alloc_base != 0); |
| assert!(pages > 0); |
| assert!(page::is_page_rounded(vaddr)); |
| self.canary.assert(); |
| |
| ltracef!("Free {pages} pages at {vaddr:#x}\n"); |
| |
| // Release the bitmap range prior to unmapping to ensure any attempts to free an invalid |
| // range are caught before attempting to unmap 'random' memory. |
| assert!(vaddr >= self.alloc_base); |
| self.bitmap_free((vaddr - self.alloc_base) / page::SIZE, pages); |
| self.unmap_free_pages(vaddr, pages); |
| } |
| |
| fn unmap_free_pages(&self, vaddr: usize, pages: usize) { |
| stack_pin_init!(let free_list = PageList::new()); |
| ltracef!("Unmapping {pages} pages at {vaddr:x}\n"); |
| for i in 0..pages { |
| let query_res = VmAspace::kernel_aspace().arch_aspace().query(vaddr + i * page::SIZE); |
| assert!(query_res.is_ok()); |
| let (paddr, _) = query_res.unwrap(); |
| let page = pmm::paddr_to_vm_page(paddr); |
| assert!(page.is_some()); |
| let page = page.unwrap(); |
| // SAFETY: `page` is an allocated page owned by this `VirtualAlloc`, is not currently |
| // in any list, and is unmapped below before `free_list` is dropped. |
| free_list.as_mut().push_back(unsafe { OwnedPage::from_allocated(page) }); |
| } |
| // As this is the kernel aspace we cannot tolerate enlarging the operation, as other CPUs |
| // may be accessing the nearby portions and we cannot trigger faults. |
| // SAFETY: The virtual range `[vaddr, vaddr + pages * page::SIZE)` is owned by this |
| // `VirtualAlloc` and is being freed. |
| let status = unsafe { |
| VmAspace::kernel_aspace().arch_aspace().unmap(vaddr, pages, ARCH_UNMAP_OPTION_NONE) |
| }; |
| assert!(status.is_ok(), "Failed to unmap {pages} pages at {vaddr:x}"); |
| } |
| |
| fn alloc_map_pages(&self, vaddr: usize, num_pages: usize) -> Result<(), Status> { |
| struct Cleanup { |
| vaddr: usize, |
| mapped_count: usize, |
| } |
| |
| impl Cleanup { |
| fn cancel(self) { |
| core::mem::forget(self); |
| } |
| } |
| |
| impl Drop for Cleanup { |
| fn drop(&mut self) { |
| if self.mapped_count > 0 { |
| // SAFETY: The virtual range `[vaddr, vaddr + mapped_count * page::SIZE)` was |
| // mapped by this call and is being unmapped on failure. |
| let status = unsafe { |
| VmAspace::kernel_aspace().arch_aspace().unmap( |
| self.vaddr, |
| self.mapped_count, |
| ARCH_UNMAP_OPTION_NONE, |
| ) |
| }; |
| assert!(status.is_ok()); |
| } |
| } |
| } |
| |
| const MMU_FLAGS: ArchMmuFlags = |
| ARCH_MMU_FLAG_CACHED | ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE; |
| assert!(num_pages > 0); |
| stack_pin_init!(let alloc_pages = PageList::new()); |
| let mut cleanup = Cleanup { vaddr, mapped_count: 0 }; |
| |
| if self.can_attempt_contiguous_mappings() { |
| let align_pages = 1usize << (self.align_log2 - page::SHIFT); |
| if align_pages > 1 { |
| while cleanup.mapped_count + align_pages <= num_pages { |
| stack_pin_init!(let contiguous_pages = PageList::new()); |
| // Being in this path we know that `align_pages` is >1, which can only happen |
| // if our `align_log2` is greater than the system `page::SHIFT`. As such we |
| // need to allocate multiple contiguous pages at a greater than system page |
| // size alignment, and so we must use the general `pmm::alloc_contiguous`. |
| let Ok(paddr) = contiguous_pages.as_mut().alloc_contiguous( |
| align_pages, |
| 0, |
| self.align_log2 as u8, |
| ) else { |
| // Failing to allocate a contiguous block isn't an error, as the pmm could |
| // just be fragmented. Drop out of this loop and attempt to finish with the |
| // single page mappings. |
| break; |
| }; |
| contiguous_pages.as_mut().set_state(self.allocated_page_state); |
| alloc_pages.as_mut().splice_front(contiguous_pages.as_mut()); |
| // SAFETY: `vaddr + mapped_count * page::SIZE` is an unmapped range owned by |
| // this `VirtualAlloc`, and `paddr` is a contiguous run of `align_pages` |
| // allocated from the PMM. |
| unsafe { |
| VmAspace::kernel_aspace().arch_aspace().map_contiguous( |
| vaddr + cleanup.mapped_count * page::SIZE, |
| paddr, |
| align_pages, |
| MMU_FLAGS, |
| )?; |
| } |
| cleanup.mapped_count += align_pages; |
| } |
| if cleanup.mapped_count == num_pages { |
| cleanup.cancel(); |
| alloc_pages.as_mut().forget_pages(); |
| return Ok(()); |
| } |
| } |
| } |
| |
| // Allocate any remaining pages. |
| stack_pin_init!(let remaining_pages = PageList::new()); |
| remaining_pages.as_mut().alloc_pages(num_pages - cleanup.mapped_count, 0)?; |
| |
| // Place them specifically at the end of any already allocated pages. This ensures that if |
| // we should iterate too far we will hit a null page and not one of our contiguous pages to |
| // ensure we can never attempt to map something twice. Due to how list_node's work this |
| // does not affect the current_page pointer we already retrieved. |
| let mut current_page = alloc_pages.as_mut().splice_back(remaining_pages.as_mut()); |
| assert!(current_page.is_valid()); |
| |
| while cleanup.mapped_count < num_pages { |
| const BATCH_PAGES: usize = 128; |
| let mut paddrs = [PAddr(0); BATCH_PAGES]; |
| let map_pages = core::cmp::min(BATCH_PAGES, num_pages - cleanup.mapped_count); |
| assert!(map_pages > 0); |
| for paddr in paddrs.iter_mut().take(map_pages) { |
| assert!(current_page.is_valid()); |
| current_page.set_state(self.allocated_page_state); |
| *paddr = current_page.paddr(); |
| current_page.move_next(); |
| } |
| |
| // SAFETY: `vaddr + mapped_count * page::SIZE` is an unmapped range owned by this |
| // `VirtualAlloc`, and `paddrs` contains `map_pages` valid physical addresses. |
| unsafe { |
| VmAspace::kernel_aspace().arch_aspace().map( |
| vaddr + cleanup.mapped_count * page::SIZE, |
| paddrs.as_mut_ptr(), |
| map_pages, |
| MMU_FLAGS, |
| ExistingEntryAction::Error, |
| )?; |
| } |
| cleanup.mapped_count += map_pages; |
| } |
| // If we successfully mapped everything we should have iterated all the way to the end of |
| // the pages we allocated. |
| assert!(!current_page.is_valid()); |
| |
| cleanup.cancel(); |
| alloc_pages.as_mut().forget_pages(); |
| |
| ltracef!("Mapped {num_pages} pages at {vaddr:x}\n"); |
| Ok(()) |
| } |
| |
| fn destroy(&mut self) { |
| self.canary.assert(); |
| // No need to destroy if not yet initialized. |
| if self.alloc_base == 0 { |
| return; |
| } |
| |
| let bitmap_pages = self.bitmap_pages(); |
| // Check that all allocated blocks were freed. Outstanding allocations indicate something |
| // is still holding a reference that they will try and use later, so we have no choice but |
| // to fail. There are more optimal ways to track outstanding allocations, but as destroying |
| // allocators is considered an extremely uncommon operation (largely just when running |
| // tests) this O(N) scan is fine. This check needs to ignore the pages for the bitmap |
| // itself, as they should still be set. |
| assert!(self.bitmap.scan(bitmap_pages, self.bitmap.size(), false).is_none()); |
| |
| // Release the pages backing the bitmap. |
| self.bitmap.storage_mut().reset(); |
| self.unmap_free_pages(self.alloc_base, bitmap_pages); |
| self.alloc_base = 0; |
| } |
| |
| /// Returns whether or not the allocator will attempt to optimize large allocations with |
| /// contiguous allocations and mappings that can get promoted to large pages. It does not |
| /// guarantee that it will succeed at doing this though. |
| pub fn can_attempt_contiguous_mappings(&self) -> bool { |
| // Ideally we would like to create the chance of using large pages with aligned allocations |
| // and `map_contiguous`, but we can only do this if we can later safely unmap without |
| // enlarging the range. |
| VmAspace::kernel_aspace().arch_aspace().unmap_only_enlarge_on_oom() |
| } |
| |
| /// Returns the number of pages allocated to provide the bitmap for tracking allocations. |
| /// Exposed as a debug named function as there is no promise that it has to be a bitmap for |
| /// tracking and this interface is likely to change and should only be used for debugging. |
| pub fn debug_bitmap_pages(&self) -> usize { |
| self.bitmap_pages() |
| } |
| |
| /// Frees any allocated pages as if `free_pages` had been called on every outstanding |
| /// allocation. This is exposed for testing for circumstances where externally tracking every |
| /// allocation is burdensome and not the goal of the test. |
| pub fn debug_free_all_allocations(&mut self) { |
| self.canary.assert(); |
| debug_assert!(self.alloc_base != 0); |
| |
| let bitmap_pages = self.bitmap_pages(); |
| |
| let mut allocated_page = bitmap_pages; |
| while let Some(page) = self.bitmap.scan(allocated_page, self.bitmap.size(), false) { |
| allocated_page = page; |
| self.free_pages(self.alloc_base + allocated_page * page::SIZE, 1); |
| } |
| } |
| |
| /// Force allocates the given range, overriding any padding or alignment requirements. This is |
| /// used for testing to more precisely control available allocation regions. Panics if range is |
| /// invalid or already allocated. |
| pub fn debug_allocate_vaddr_range(&mut self, vaddr: usize, num_pages: usize) { |
| self.canary.assert(); |
| assert!(page::is_page_rounded(vaddr)); |
| assert!(num_pages > 0); |
| assert!(vaddr >= self.alloc_base + self.bitmap_pages() * page::SIZE); |
| |
| let index = (vaddr - self.alloc_base) >> page::SHIFT; |
| |
| assert!(self.bitmap.scan(index, index + num_pages, false).is_none()); |
| let _ = self.bitmap.set(index, index + num_pages); |
| let _ = self.alloc_map_pages(vaddr, num_pages); |
| } |
| |
| fn bitmap_pages(&self) -> usize { |
| self.canary.assert(); |
| assert!(self.alloc_base != 0); |
| |
| self.bitmap.storage().get_size() / page::SIZE |
| } |
| } |
| |
| /// Unit tests for `VirtualAlloc`. |
| #[cfg(ktest)] |
| #[unittest::suite(name = "virtual_alloc_rust")] |
| mod tests { |
| use super::VirtualAlloc; |
| use super::page_list::{OwnedPage, PageList}; |
| use crate::vm::arch_vm_aspace::{ |
| ARCH_MMU_FLAG_CACHED, ARCH_MMU_FLAG_PERM_READ, ARCH_MMU_FLAG_PERM_WRITE, |
| ARCH_UNMAP_OPTION_NONE, ExistingEntryAction, |
| }; |
| use crate::vm::page_state::{VmPageState, bindings as page_bindings}; |
| use crate::vm::scanner::AutoVmScannerDisable; |
| use crate::vm::vm_address_region::{VmAddressRegion, flag as vmar_flag}; |
| use crate::vm::vm_aspace::VmAspace; |
| use core::ops::Deref; |
| use core::sync::atomic::{AtomicU64, Ordering}; |
| use fbl::RefPtr; |
| use pin_init::stack_pin_init; |
| use unittest::{ |
| assert_err, assert_false, assert_ok, assert_true, expect_eq, expect_err, expect_false, |
| expect_gt, expect_lt, expect_ne, expect_ok, expect_true, unwrap_ok, |
| }; |
| use zx_status::Status; |
| |
| const MB: usize = 1024 * 1024; |
| const GB: usize = 1024 * 1024 * 1024; |
| const TEST_HEAP_SIZE: usize = 16 * MB; |
| // We pick 2MiB for the heap alignment as this is a large page on many architectures. Having |
| // this be a large page is not actually necessary for this test coverage, but it serves as some |
| // bonus mmu testing so we might as well. |
| const TEST_HEAP_ALIGN_LOG2: usize = 21; |
| const ALIGN_PAGES: usize = 1usize << (TEST_HEAP_ALIGN_LOG2 - page::SHIFT); |
| |
| // Helper that checks if there exists any contiguous blocks in the pmm. This can be used by |
| // tests to attempt to avoid spurious failures. |
| fn can_expect_contiguous() -> bool { |
| stack_pin_init!(let pages = PageList::new()); |
| |
| if pages.as_mut().alloc_contiguous(ALIGN_PAGES, 0, TEST_HEAP_ALIGN_LOG2 as u8).is_err() { |
| return false; |
| } |
| |
| // Allocate some additional pages, do not need to be contiguous. |
| if pages.as_mut().alloc_pages(ALIGN_PAGES, 0).is_err() { |
| return false; |
| } |
| |
| // Returns the pages in a jumbled order. This ensures that if, in common case where the pmm |
| // is running in LIFO mode, the test were to use `pmm::alloc_pages` instead of |
| // `pmm::alloc_contiguous` it will be guaranteed to *not* get contiguous pages, preventing |
| // any spurious success. |
| // |
| // See https://fxbug.dev/349402040 for context. |
| let mut heads_tails = true; |
| while !pages.is_empty() { |
| let page = |
| if heads_tails { pages.as_mut().pop_front() } else { pages.as_mut().pop_back() } |
| .expect("list is non-empty"); |
| drop(page); |
| heads_tails = !heads_tails; |
| } |
| true |
| } |
| |
| fn range_empty(base: usize, num_pages: usize) -> bool { |
| for i in 0..num_pages { |
| if VmAspace::kernel_aspace().arch_aspace().query(base + i * page::SIZE).is_ok() { |
| return false; |
| } |
| } |
| true |
| } |
| |
| fn range_contiguous(base: usize, num_pages: usize) -> bool { |
| assert!(num_pages > 0); |
| // Get the paddr of the first page. |
| let Ok((base_paddr, _)) = VmAspace::kernel_aspace().arch_aspace().query(base) else { |
| return false; |
| }; |
| // Check all subsequent pages are physically contiguous. |
| for i in 1..num_pages { |
| let Ok((paddr, _)) = |
| VmAspace::kernel_aspace().arch_aspace().query(base + i * page::SIZE) |
| else { |
| return false; |
| }; |
| if paddr.0 != base_paddr.0 + i * page::SIZE { |
| return false; |
| } |
| } |
| true |
| } |
| |
| // Helper to construct and teardown a vmar for testing. |
| struct TestVmar { |
| vmar: RefPtr<VmAddressRegion>, |
| } |
| |
| impl TestVmar { |
| fn new() -> Self { |
| let root_vmar = |
| VmAspace::kernel_aspace().root_vmar().expect("kernel root vmar must exist"); |
| let vmar = root_vmar |
| .create_sub_vmar( |
| 0, // 0 offset to request random placement |
| TEST_HEAP_SIZE, |
| TEST_HEAP_ALIGN_LOG2 as u8, |
| vmar_flag::CAN_MAP_READ | vmar_flag::CAN_MAP_WRITE, |
| b"virtual_alloc test", |
| ) |
| .expect("CreateSubVmar must succeed"); |
| Self { vmar } |
| } |
| |
| fn region_empty(&self) -> bool { |
| range_empty(self.vmar.base().0, self.vmar.size() / page::SIZE) |
| } |
| } |
| |
| impl Deref for TestVmar { |
| type Target = VmAddressRegion; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.vmar |
| } |
| } |
| |
| impl Drop for TestVmar { |
| fn drop(&mut self) { |
| assert!(self.region_empty()); |
| let _ = self.vmar.destroy(); |
| } |
| } |
| |
| // Helper for touching a pages in a virtual address range to ensure they can be accessed |
| // without faulting. By default a unique value is written to and then read from every page, |
| // writing can be optionally disabled to validate that the contents of the range have not |
| // changed since last time. |
| fn touch_pages(vaddr: usize, num_pages: usize, write: bool) -> bool { |
| for page_base in (vaddr..vaddr + (num_pages * page::SIZE)).step_by(page::SIZE) { |
| let ptr = core::ptr::with_exposed_provenance_mut::<u64>(page_base); |
| // SAFETY: `page_base` is a page-aligned mapped virtual address owned by the caller. |
| let var = unsafe { AtomicU64::from_ptr(ptr) }; |
| if write { |
| var.store(page_base as u64, Ordering::Relaxed); |
| } |
| if var.load(Ordering::Relaxed) != page_base as u64 { |
| return false; |
| } |
| } |
| true |
| } |
| |
| /// Test that some simple ways to use a virtual allocator work as expected. |
| #[test] |
| fn virtual_alloc_smoke_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT)); |
| |
| let result = alloc.alloc_pages(8); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| expect_true!(touch_pages(result, 8, true)); |
| |
| let result2 = alloc.alloc_pages(8); |
| assert_true!(result2.is_ok()); |
| let result2 = unwrap_ok!(result2); |
| expect_ne!(result, result2); |
| expect_true!(touch_pages(result2, 8, true)); |
| expect_true!(touch_pages(result, 8, false)); |
| |
| expect_false!(alloc.alloc_pages(GB).is_ok()); |
| |
| alloc.free_pages(result, 8); |
| alloc.free_pages(result2, 4); |
| alloc.free_pages(result2 + 4 * page::SIZE, 4); |
| } |
| |
| /// Tests minimum valid sizes for initializing the allocator. |
| #[test] |
| fn virtual_alloc_valid_size_test() { |
| let vmar = TestVmar::new(); |
| |
| { |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| // This would only hold the bitmap, but not the padding for it |
| assert_err!( |
| alloc.init(vmar.base().0, page::SIZE, 16, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| assert_err!( |
| alloc.init(vmar.base().0, 16 * page::SIZE, 16, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| // Would hold the bitmap and one padding. |
| assert_err!( |
| alloc.init(vmar.base().0, 17 * page::SIZE, 16, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| // Would hold bitmap and two paddings, but still cannot actually allocate a page. |
| assert_err!( |
| alloc.init(vmar.base().0, 33 * page::SIZE, 16, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| // Succeeds, and should support a single page of allocation. |
| expect_ok!(alloc.init(vmar.base().0, 34 * page::SIZE, 16, page::SHIFT)); |
| let result = alloc.alloc_pages(1); |
| expect_true!(result.is_ok()); |
| // Should not be able to do additional allocations. |
| expect_false!(alloc.alloc_pages(1).is_ok()); |
| alloc.free_pages(unwrap_ok!(result), 1); |
| expect_false!(alloc.alloc_pages(2).is_ok()); |
| } |
| expect_true!(vmar.region_empty()); |
| |
| { |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| // Two allocations should have a single padding between them, not double padding. To |
| // have two allocations of 1 page we should have a layout of |
| // [bitmap(1)] [padding(16)] [allocation(1)] [padding(16)] [allocation(1)] [padding(16) |
| // = 51. |
| assert_ok!(alloc.init(vmar.base().0, 51 * page::SIZE, 16, page::SHIFT)); |
| let result1 = alloc.alloc_pages(1); |
| expect_true!(result1.is_ok()); |
| let result2 = alloc.alloc_pages(1); |
| expect_true!(result2.is_ok()); |
| expect_false!(alloc.alloc_pages(1).is_ok()); |
| alloc.free_pages(unwrap_ok!(result1), 1); |
| alloc.free_pages(unwrap_ok!(result2), 1); |
| } |
| expect_true!(vmar.region_empty()); |
| } |
| |
| /// Tests that repeated freeing and allocating reuses the same compact virtual addresses. |
| #[test] |
| fn virtual_alloc_compact_test() { |
| let vmar = TestVmar::new(); |
| |
| const NUM_ALLOC: usize = 8; |
| let mut allocs = [0usize; NUM_ALLOC]; |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 2, page::SHIFT)); |
| |
| for slot in &mut allocs { |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| *slot = unwrap_ok!(result); |
| touch_pages(*slot, 3, true); |
| } |
| for &addr in &allocs { |
| touch_pages(addr, 3, false); |
| } |
| |
| // We should now be able to repeatedly free and alloc one of the middle allocations and |
| // have it continuously give us the same virtual address. |
| for _ in 0..200 { |
| alloc.free_pages(allocs[NUM_ALLOC / 2], 3); |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| expect_eq!(allocs[NUM_ALLOC / 2], unwrap_ok!(result)); |
| touch_pages(allocs[NUM_ALLOC / 2], 3, true); |
| } |
| |
| // Freeing an allocation in the middle, then the end, should first reuse the middle and |
| // then the end. |
| alloc.free_pages(allocs[NUM_ALLOC / 2], 3); |
| alloc.free_pages(allocs[NUM_ALLOC - 1], 3); |
| |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| expect_eq!(allocs[NUM_ALLOC / 2], unwrap_ok!(result)); |
| touch_pages(allocs[NUM_ALLOC / 2], 3, true); |
| |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| expect_eq!(allocs[NUM_ALLOC - 1], unwrap_ok!(result)); |
| touch_pages(allocs[NUM_ALLOC - 1], 3, true); |
| |
| // Now if we free everything and realloc, should get the same starting address. |
| for &addr in &allocs { |
| touch_pages(addr, 3, false); |
| alloc.free_pages(addr, 3); |
| } |
| |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| expect_eq!(allocs[0], unwrap_ok!(result)); |
| touch_pages(allocs[0], 3, true); |
| |
| // Cleanup. |
| alloc.free_pages(allocs[0], 3); |
| } |
| |
| /// Tests that partial frees move the search start back appropriately. |
| #[test] |
| fn virtual_alloc_partial_free_compact_test() { |
| let vmar = TestVmar::new(); |
| |
| const NUM_ALLOC: usize = 8; |
| let mut allocs = [0usize; NUM_ALLOC]; |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 2, page::SHIFT)); |
| |
| for slot in &mut allocs { |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| *slot = unwrap_ok!(result); |
| touch_pages(*slot, 3, true); |
| } |
| |
| // Free one of the middle allocations, and part of the one before it. |
| alloc.free_pages(allocs[NUM_ALLOC / 2], 3); |
| alloc.free_pages(allocs[NUM_ALLOC / 2 - 1] + page::SIZE, 2); |
| |
| // Now when we alloc we should get something earlier than the full allocation that we |
| // freed. |
| let result = alloc.alloc_pages(3); |
| expect_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| expect_lt!(result, allocs[NUM_ALLOC / 2]); |
| expect_gt!(result, allocs[NUM_ALLOC / 2 - 1]); |
| touch_pages(result, 3, true); |
| |
| // Finish freeing the early allocation. A new full allocation should no longer fit anywhere |
| // and have to go at the end. |
| alloc.free_pages(allocs[NUM_ALLOC / 2 - 1], 1); |
| |
| let result2 = alloc.alloc_pages(3); |
| expect_true!(result2.is_ok()); |
| let result2 = unwrap_ok!(result2); |
| expect_gt!(result2, allocs[NUM_ALLOC - 1]); |
| touch_pages(result2, 3, true); |
| |
| // cleanup all the allocs. |
| allocs[NUM_ALLOC / 2 - 1] = result; |
| allocs[NUM_ALLOC / 2] = result2; |
| |
| for &addr in &allocs { |
| touch_pages(addr, 3, false); |
| alloc.free_pages(addr, 3); |
| } |
| } |
| |
| /// Tests large multi-batch allocations. |
| #[test] |
| fn virtual_alloc_large_alloc_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 2, page::SHIFT)); |
| |
| // Create and validate some large allocations to ensure the batching in the mapping path |
| // works correctly. |
| let result = alloc.alloc_pages(128); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| touch_pages(result, 128, true); |
| alloc.free_pages(result, 128); |
| |
| let result = alloc.alloc_pages(250); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| touch_pages(result, 250, true); |
| alloc.free_pages(result, 250); |
| } |
| |
| /// Tests rollback cleanup when an arch aspace mapping fails partway through. |
| #[test] |
| fn virtual_alloc_arch_alloc_failure_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 2, page::SHIFT)); |
| |
| // Perform an allocation to see what virtual address we expect to be using. |
| let result = alloc.alloc_pages(250); |
| assert_true!(result.is_ok()); |
| let vaddr = unwrap_ok!(result); |
| touch_pages(vaddr, 250, true); |
| alloc.free_pages(vaddr, 250); |
| |
| // Let's map our own page towards the end of the allocation. |
| let (_page, mut page_paddr) = unwrap_ok!(OwnedPage::alloc(0)); |
| |
| // SAFETY: `vaddr + 240 * page::SIZE` is within `vmar` and unmapped, and `page_paddr` is |
| // valid. |
| expect_ok!(unsafe { |
| VmAspace::kernel_aspace().arch_aspace().map( |
| vaddr + 240 * page::SIZE, |
| &mut page_paddr, |
| 1, |
| ARCH_MMU_FLAG_CACHED | ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, |
| ExistingEntryAction::Error, |
| ) |
| }); |
| |
| // Attempting our allocation again should fail. |
| let result = alloc.alloc_pages(250); |
| expect_false!(result.is_ok()); |
| |
| // No other pages should have gotten mapped in though as once our page was discovered |
| // everything should have been unmapped and cleaned up. |
| expect_true!(range_empty(vaddr, 240)); |
| |
| // Once we unmap our page we should successfully allocate the original mapping. |
| // SAFETY: `vaddr + 240 * page::SIZE` was mapped above. |
| expect_ok!(unsafe { |
| VmAspace::kernel_aspace().arch_aspace().unmap( |
| vaddr + 240 * page::SIZE, |
| 1, |
| ARCH_UNMAP_OPTION_NONE, |
| ) |
| }); |
| |
| let result = alloc.alloc_pages(250); |
| assert_true!(result.is_ok()); |
| expect_eq!(vaddr, unwrap_ok!(result)); |
| touch_pages(vaddr, 250, true); |
| |
| alloc.free_pages(vaddr, 250); |
| } |
| |
| /// Tests that allocating zero pages returns `INVALID_ARGS`. |
| #[test] |
| fn virtual_alloc_zero_pages_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT)); |
| |
| // Allocating zero pages is considered an error. |
| expect_err!(alloc.alloc_pages(0), Status::INVALID_ARGS); |
| } |
| |
| /// Tests allocator initialization error cases. |
| #[test] |
| fn virtual_alloc_init_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| |
| // Any allocation should fail. |
| expect_err!(alloc.alloc_pages(1), Status::BAD_STATE); |
| expect_err!(alloc.alloc_pages(0), Status::BAD_STATE); |
| |
| // Bases and sizes need to be aligned |
| expect_err!( |
| alloc.init(vmar.base().0 + 1, vmar.size(), 1, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| expect_err!( |
| alloc.init(vmar.base().0 + 1, vmar.size() + 1, 1, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| expect_err!( |
| alloc.init(vmar.base().0, vmar.size() + 1, 1, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| expect_err!( |
| alloc.init(vmar.base().0 + 1, vmar.size() - 1, 1, page::SHIFT), |
| Status::INVALID_ARGS |
| ); |
| |
| // Require at least page size alignment |
| expect_err!(alloc.init(vmar.base().0, vmar.size(), 1, 0), Status::INVALID_ARGS); |
| expect_err!( |
| alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT - 1), |
| Status::INVALID_ARGS |
| ); |
| |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT)); |
| |
| // Should not be able to re-init |
| expect_err!(alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT), Status::BAD_STATE); |
| } |
| |
| /// Tests aligned allocations and fallback to unaligned allocations. |
| #[test] |
| fn virtual_alloc_aligned_alloc_test() { |
| let _scanner_disable = AutoVmScannerDisable::new(); |
| |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| |
| expect_ok!(alloc.init(vmar.base().0, vmar.size(), 1, TEST_HEAP_ALIGN_LOG2)); |
| |
| // Create a large allocation that we will use for future allocations. Size it under the |
| // assumption that the bitmap and padding will only use up a single aligned block. |
| const LARGE_ALLOC_PAGES: usize = (TEST_HEAP_SIZE >> page::SHIFT) - ALIGN_PAGES * 2; |
| // Validate that we have at least a few alignment multiples so that any maths we do below |
| // is guaranteed to not overflow. |
| zr::static_assert!(LARGE_ALLOC_PAGES / ALIGN_PAGES > 5); |
| zr::static_assert!(LARGE_ALLOC_PAGES.is_multiple_of(ALIGN_PAGES)); |
| let result = alloc.alloc_pages(LARGE_ALLOC_PAGES); |
| assert_true!(result.is_ok()); |
| |
| // Record the base address of the large block. We'll use this to calculate all our test |
| // areas. |
| let base_test_vaddr = unwrap_ok!(result); |
| |
| // Alloc single pages until failure to ensure that in the future any allocations can only |
| // succeed where we and when we want them to. |
| while alloc.alloc_pages(1).is_ok() {} |
| |
| // Free a range in the middle such that with padding and alignment taken into account a |
| // single large allocation would fit. |
| alloc.free_pages(base_test_vaddr + (ALIGN_PAGES * 2 - 1) * page::SIZE, ALIGN_PAGES + 2); |
| let contiguous = alloc.can_attempt_contiguous_mappings() && can_expect_contiguous(); |
| let result = alloc.alloc_pages(ALIGN_PAGES); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| expect_eq!(base_test_vaddr + ALIGN_PAGES * 2 * page::SIZE, result); |
| expect_true!(!contiguous || range_contiguous(result, ALIGN_PAGES)); |
| |
| // Free the range and re-allocate the lowest page in the gap. |
| alloc.free_pages(result, ALIGN_PAGES); |
| alloc.debug_allocate_vaddr_range(base_test_vaddr + (ALIGN_PAGES * 2 - 1) * page::SIZE, 1); |
| |
| // Gap should be too small to allocate `ALIGN_PAGES` now. |
| assert_false!(alloc.alloc_pages(ALIGN_PAGES).is_ok()); |
| |
| // Free another page higher up. This should allow the allocation to succeed, although we |
| // can make no claims on it being contiguous, since it's no longer aligned. |
| alloc.free_pages(base_test_vaddr + (ALIGN_PAGES * 3 + 1) * page::SIZE, 1); |
| let result = alloc.alloc_pages(ALIGN_PAGES); |
| assert_true!(result.is_ok()); |
| alloc.free_pages(unwrap_ok!(result), ALIGN_PAGES); |
| |
| // Free a large range before and after this, but it should still use the aligned range even |
| // though it creates fragmentation that prevents future allocations. |
| alloc.free_pages(base_test_vaddr + ALIGN_PAGES * page::SIZE, ALIGN_PAGES); |
| alloc.free_pages(base_test_vaddr + (ALIGN_PAGES * 3 + 2) * page::SIZE, ALIGN_PAGES - 2); |
| let contiguous = alloc.can_attempt_contiguous_mappings() && can_expect_contiguous(); |
| let result = alloc.alloc_pages(ALIGN_PAGES); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| expect_eq!(base_test_vaddr + ALIGN_PAGES * 2 * page::SIZE, result); |
| expect_true!(!contiguous || range_contiguous(result, ALIGN_PAGES)); |
| expect_false!(alloc.alloc_pages(ALIGN_PAGES).is_ok()); |
| |
| // We didn't properly track our allocations but it's not the goal of this test so just ask |
| // the allocator to clean up for us. |
| alloc.debug_free_all_allocations(); |
| } |
| |
| /// Tests that large aligned allocations use contiguous physical mappings when available. |
| #[test] |
| fn virtual_alloc_large_allocs_are_contiguous_test() { |
| let _scanner_disable = AutoVmScannerDisable::new(); |
| |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| |
| expect_ok!(alloc.init(vmar.base().0, vmar.size(), 1, TEST_HEAP_ALIGN_LOG2)); |
| |
| if alloc.can_attempt_contiguous_mappings() && can_expect_contiguous() { |
| let result = alloc.alloc_pages(ALIGN_PAGES); |
| assert_true!(result.is_ok()); |
| let result = unwrap_ok!(result); |
| expect_true!(range_contiguous(result, ALIGN_PAGES)); |
| alloc.free_pages(result, ALIGN_PAGES); |
| } else { |
| kprint::kprintln!( |
| "Failed to find contiguous range of {ALIGN_PAGES} pages, \ |
| skipping test: virtual_alloc_large_allocs_are_contiguous_test" |
| ); |
| } |
| } |
| |
| /// Tests fallback to non-contiguous pages when contiguous PMM blocks are exhausted. |
| #[test] |
| fn virtual_alloc_contiguous_fallback_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| |
| expect_ok!(alloc.init(vmar.base().0, vmar.size(), 1, TEST_HEAP_ALIGN_LOG2)); |
| |
| stack_pin_init!(let pages = PageList::new()); |
| |
| // Now steal all the contiguous blocks out of the pmm. |
| stack_pin_init!(let temp_pages = PageList::new()); |
| while temp_pages |
| .as_mut() |
| .alloc_contiguous(ALIGN_PAGES, 0, TEST_HEAP_ALIGN_LOG2 as u8) |
| .is_ok() |
| { |
| // Keep the first page and return the rest. This prevents us OOMing whilst still |
| // blocking future allocations. We only need to hold one page since this allocation is |
| // size aligned and so no page is a candidate for any other allocation. |
| let head_page = temp_pages.as_mut().pop_front().expect("head_page must exist"); |
| pages.as_mut().push_front(head_page); |
| temp_pages.as_mut().free_pages(); |
| } |
| |
| // A size aligned allocation should still work, as it should fall back to using |
| // non-contiguous pages. |
| let result = alloc.alloc_pages(ALIGN_PAGES); |
| assert_true!(result.is_ok()); |
| alloc.free_pages(unwrap_ok!(result), ALIGN_PAGES); |
| } |
| |
| /// Tests allocating and freeing the first and last valid pages of the managed range. |
| #[test] |
| fn virtual_alloc_free_boundary_test() { |
| let vmar = TestVmar::new(); |
| |
| let mut alloc = VirtualAlloc::new(VmPageState(page_bindings::vm_page_state::ALLOC)); |
| assert_ok!(alloc.init(vmar.base().0, vmar.size(), 1, page::SHIFT)); |
| |
| let bitmap_pages = alloc.debug_bitmap_pages(); |
| let first_page_vaddr = vmar.base().0 + bitmap_pages * page::SIZE; |
| let last_page_vaddr = vmar.base().0 + vmar.size() - page::SIZE; |
| |
| // Allocate and free the first valid page |
| alloc.debug_allocate_vaddr_range(first_page_vaddr, 1); |
| assert_true!(touch_pages(first_page_vaddr, 1, true)); |
| alloc.free_pages(first_page_vaddr, 1); |
| |
| // Allocate and free the last valid page |
| alloc.debug_allocate_vaddr_range(last_page_vaddr, 1); |
| assert_true!(touch_pages(last_page_vaddr, 1, true)); |
| alloc.free_pages(last_page_vaddr, 1); |
| } |
| } |