blob: 5340dbf6ede55a3bc09a7c286ca1ece8fb123061 [file]
// 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);
}
}