blob: a4d61d8b281611506f3db585c513dec0bd8f8a3c [file] [log] [blame] [edit]
//! Raw Allocator
//!
//! This module exposes the raw handlers behind the UEFI allocator. The
//! allocator traits of the standard library are marked as unstable, hence this
//! module provides stable access to the same functionality, if required.
//!
//! Use of the raw allocator is only recommended if the other exposes APIs are
//! not an option.
use r_efi::efi;
// UEFI guarantees 8-byte alignments through `AllocatePool()`. Any request
// higher than this alignment needs to take special precautions to align the
// returned pointer, and revert that step when freeing the memory block again.
const POOL_ALIGNMENT: usize = 8usize;
// Alignment Marker
//
// Since UEFI has no functions to allocate blocks of arbitrary alignment, we
// have to work around this. We extend the allocation size by the required
// alignment and then offset the pointer before returning it. This will
// properly align the pointer to the given request.
//
// However, when freeing memory again, we have to somehow get back the original
// pointer. Therefore, we store the original address directly in front of the
// memory block that we just aligned. When freeing memory, we simply retrieve
// this marker and free the original address.
#[repr(C)]
struct Marker(*mut u8);
fn align_request(size: usize, align: usize) -> usize {
// If the alignment request is within UEFI guarantees, there is no need to
// adjust the size request. In all other cases, we might have to align the
// allocated memory block. Hence, we increment the request size by the
// alignment size. Strictly speaking, we only need `align - POOL_ALIGNMENT`
// as additional space, since the pool alignment is always guaranteed by
// UEFI. However, by adding the full alignment we are guaranteed
// `POOL_ALIGNMENT` extra space. This extra space is used to store a marker
// so we can retrieve the original pointer when freeing the memory space.
if align > POOL_ALIGNMENT {
size + align
} else {
size
}
}
unsafe fn align_block(ptr: *mut u8, align: usize) -> *mut u8 {
// This function takes a pointer returned by the pool-allocator, and aligns
// it to the requested alignment. If this alignment is smaller than the
// guaranteed pool alignment, there is nothing to be done. If it is bigger,
// we will have to offset the pointer. We rely on the caller using
// `align_request()` to increase the allocation size beforehand. We then
// store the original address as `Marker` in front of the aligned pointer,
// so `unalign_block()` can retrieve it again.
if align > POOL_ALIGNMENT {
// In `align_request()` we guarantee the allocation size includes an
// additional `align` bytes. Since the pool allocation already
// guaranteed an alignment of `POOL_ALIGNMENT`, we know that
// `offset >= POOL_ALIGNMENT` here. We then verify that
// `POOL_ALIGNMENT` serves the needs of our `Marker` object. Note that
// all but the first assertion are constant expressions, so the
// compiler will optimize them away.
let offset = align - (ptr as usize & (align - 1));
assert!(offset >= POOL_ALIGNMENT);
assert!(POOL_ALIGNMENT >= core::mem::size_of::<Marker>());
assert!(POOL_ALIGNMENT >= core::mem::align_of::<Marker>());
// We calculated the alignment-offset, so adjust the pointer and store
// the original address directly in front. This will allow
// `unalign_block()` to retrieve the original address, so it can free
// the entire memory block.
let aligned = ptr.add(offset);
core::ptr::write((aligned as *mut Marker).offset(-1), Marker(ptr));
aligned
} else {
ptr
}
}
unsafe fn unalign_block(ptr: *mut u8, align: usize) -> *mut u8 {
// This undoes what `align_block()` did. That is, we retrieve the original
// address that was stored directly in front of the aligned block, and
// return it to the caller. Note that this is only the case if the
// alignment exceeded the guaranteed alignment of the allocator.
if align > POOL_ALIGNMENT {
core::ptr::read((ptr as *mut Marker).offset(-1)).0
} else {
ptr
}
}
/// Allocate Memory from UEFI Boot-Services
///
/// Use the UEFI `allocate_pool` boot-services to request a block of memory
/// satisfying the given memory layout. The `memory_type` parameter specifies
/// which UEFI allocator to use.
///
/// This returns a null-pointer if the allocator could not serve the request
/// (which on UEFI implies out-of-memory). Otherwise, a non-null pointer to
/// the aligned block is returned.
///
/// Safety
/// ------
///
/// To ensure safety of this interface, the caller must guarantee:
///
/// * The allocation size must not be 0. The function will panic otherwise.
///
/// * It must be safe for this function to call `allocate_pool` of the
/// boot-services provided via the system-table. It is the responsibility of
/// the caller to retain boot-services until the returned allocation is
/// released via `dealloc()`, or to account for it otherwise.
///
/// * The returned pointer is not necessarily the same pointer as returned
/// by `allocate_pool` of the boot-services. A caller must not assume this
/// when forwarding the pointer to other allocation services.
pub unsafe fn alloc(
system_table: *mut efi::SystemTable,
layout: core::alloc::Layout,
memory_type: efi::MemoryType,
) -> *mut u8 {
// `Layout` guarantees the size+align combination does not overflow.
let align = layout.align();
let size = layout.size();
// Verify our increased requirements are met.
assert!(size > 0);
// We need extra allocation space to guarantee large alignment requests. If
// `size+align` overflows, there will be insufficient address-space for the
// request, so make it fail early.
if size.checked_add(align).is_none() {
return core::ptr::null_mut();
}
// We forward the allocation request to `AllocatePool()`. This takes the
// memory-type and size as argument, and places a pointer to the allocation
// in an output argument. Note that UEFI guarantees 8-byte alignment (i.e.,
// `POOL_ALIGNMENT`). To support higher alignments, see the
// `align_request() / align_block() / unalign_block()` helpers.
let mut ptr: *mut core::ffi::c_void = core::ptr::null_mut();
let size_allocated = align_request(size, align);
let r = unsafe {
((*(*system_table).boot_services).allocate_pool)(
memory_type,
size_allocated,
&mut ptr,
)
};
// The only real error-scenario is OOM ("out-of-memory"). UEFI does not
// clearly specify what a return value of NULL+success means (but indicates
// in a lot of cases that NULL is never a valid pointer). Furthermore,
// since the 0-page is usually unmapped and not available for
// EFI_CONVENTIONAL_MEMORY, a NULL pointer cannot be a valid return
// pointer. Therefore, we treat both a function failure as well as a NULL
// pointer the same.
// No known UEFI implementation returns `NULL`, hence this is mostly a
// safety net in case any unknown implementation fails to adhere.
if r.is_error() || ptr.is_null() {
core::ptr::null_mut()
} else {
unsafe { align_block(ptr as *mut u8, align) }
}
}
/// Deallocate Memory from UEFI Boot-Services
///
/// Use the UEFI `free_pool` boot-services to release a block of memory
/// previously allocated through `alloc()`.
///
/// Safety
/// ------
///
/// The memory block must be the same as previously returned by `alloc()`.
/// Furthermore, this function must be able to call the UEFI boot-servies
/// through the specified system table, and this must match the same
/// boot-services the memory block was allocated through.
///
/// The passed layout must match the layout used to allocate the memory block.
pub unsafe fn dealloc(
system_table: *mut efi::SystemTable,
ptr: *mut u8,
layout: core::alloc::Layout,
) {
// UEFI never allows null-pointers for allocations, hence such a pointer
// cannot have been retrieved through `alloc()` previously.
assert!(!ptr.is_null());
// Un-align the pointer to get access to the actual start of the block.
let original = unalign_block(
ptr,
layout.align(),
) as *mut core::ffi::c_void;
// Release the memory block via the boot-services.
let r = ((*(*system_table).boot_services).free_pool)(original);
// The spec allows returning errors from `FreePool()`. However, it
// must serve any valid requests. Only `INVALID_PARAMETER` is
// listed as possible error. Hence, there is no point in forwarding
// the return value. We still assert on it to improve diagnostics
// in early-boot situations. This should be a negligible
// performance penalty.
assert!(!r.is_error());
}
#[cfg(test)]
mod tests {
use super::*;
// Test the `align_request()` helper and verify that it correctly
// calculates the supported alignment requests.
#[test]
fn align() {
let ptrsize = std::mem::size_of::<*mut ()>();
// UEFI ABI specifies that allocation alignment minimum is always 8. So
// this can be statically verified.
assert_eq!(POOL_ALIGNMENT, 8);
// Loop over allocation-request sizes from 0-256 and alignments from
// 1-128, and verify that in case of overalignment there is at least
// space for one additional pointer to store in the allocation.
for i in 0..256 {
for j in &[1, 2, 4, 8, 16, 32, 64, 128] {
if *j <= 8 {
assert_eq!(align_request(i, *j), i);
} else {
assert!(align_request(i, *j) > i + ptrsize);
}
}
}
}
}