| // Copyright 2016 The Fuchsia Authors | |
| // Copyright (c) 2015 Google, Inc. All rights reserved | |
| // | |
| // 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 | |
| //! Malloc implementation tuned for space. | |
| //! | |
| //! Allocation strategy takes place with a global mutex. Freelist entries are | |
| //! kept in linked lists with 8 different sizes per binary order of magnitude | |
| //! and the header size is two words with eager coalescing on free. | |
| //! | |
| //! ## Concepts ## | |
| //! | |
| //! OS allocation: | |
| //! A contiguous range of pages allocated from the OS using `heap_page_alloc()`, | |
| //! typically via `heap_grow()`. Initial layout: | |
| //! | |
| //! Low addr => | |
| //! `Header` `left_sentinel` -- Marked as allocated, `left` pointer NULL. | |
| //! `Free` `memory_area` -- Marked as free, with appropriate size, | |
| //! and pointed to by a free bucket. | |
| //! [bulk of usable memory] | |
| //! `Header` `right_sentinel` -- Marked as allocated, size zero | |
| //! <= High addr | |
| //! | |
| //! For a normal allocation, the free memory area is added to the | |
| //! appropriate free bucket and picked up later in the `cmpct_alloc()` | |
| //! logic. For a large allocation, the area skips the primary free buckets | |
| //! and is returned directly via a `*mut *mut Free` bucket param. | |
| //! | |
| //! `cmpctmalloc` does not keep a list of OS allocations; each is meant to free | |
| //! itself to the OS when all of its memory areas become free. | |
| //! | |
| //! Memory area: | |
| //! A sub-range of an OS allocation. Used to satisfy | |
| //! `cmpct_alloc()`/`cmpct_memalign()` calls. Can be free and live in a free | |
| //! bucket, or can be allocated and managed by the user. | |
| //! | |
| //! Memory areas, both free and allocated, always begin with a `Header`, | |
| //! followed by the area's usable memory. `Header.size` includes the size of | |
| //! the header. `untag(Header.left)` points to the preceding area's `Header`. | |
| //! | |
| //! The low bits of `Header.left` hold additional flags about the area: | |
| //! - `FREE_BIT`: The area is free, and lives in a free bucket. | |
| //! | |
| //! These bits shouldn't be checked directly; use the `is_tagged_as_*()` | |
| //! functions. | |
| //! | |
| //! If the area is free (`is_tagged_as_free(*const Header)`), the area's header | |
| //! includes the doubly-linked free list pointers defined by `Free` (which is a | |
| //! `Header` overlay). Those pointers are used to chain the free area off of | |
| //! the appropriately-sized free bucket. | |
| //! | |
| //! Normal (small/non-large) allocation: | |
| //! An allocation of less than or equal to (`HEAP_LARGE_ALLOC_BYTES - size_of::<Header>()`), | |
| //! which can fit in a free bucket. | |
| //! | |
| //! Large allocation: | |
| //! An allocation of more than (`HEAP_LARGE_ALLOC_BYTES - size_of::<Header>()`). This | |
| //! is no longer allowed. | |
| //! | |
| //! Free buckets: | |
| //! Freelist entries are kept in linked lists with 8 different sizes per binary | |
| //! order of magnitude: `heap.free_lists[NUMBER_OF_BUCKETS]` | |
| //! | |
| //! Allocations are always rounded up to the nearest bucket size. This would | |
| //! appear to waste memory, but in fact it avoids some fragmentation. | |
| //! | |
| //! Consider two buckets with size 512 and 576 (512 + 64). Perhaps the program | |
| //! often allocates 528 byte objects for some reason. When we need to allocate | |
| //! 528 bytes, we round that up to 576 bytes. When it is freed, it goes in the | |
| //! 576 byte bucket, where it is available for the next of the common 528 byte | |
| //! allocations. | |
| //! | |
| //! If we did not round up allocations, then (assuming no coalescing is | |
| //! possible) we would have to place the freed 528 bytes in the 512 byte | |
| //! bucket, since only memory areas greater than or equal to 576 bytes can go | |
| //! in the 576 byte bucket. The next time we need to allocate a 528 byte object | |
| //! we do not look in the 512 byte bucket, because we want to be sure the first | |
| //! memory area we look at is big enough, to avoid searching a long chain of | |
| //! just-too-small memory areas on the free list. We would not find the 528 | |
| //! byte space and would have to carve out a new 528 byte area from a large | |
| //! free memory area, making fragmentation worse. | |
| //! | |
| //! `cmpct_free()` behavior: | |
| //! Freed memory areas are eagerly coalesced with free left/right neighbors. If | |
| //! the new free area covers an entire OS allocation (i.e., its left and right | |
| //! neighbors are both sentinels), the OS allocation is returned to the OS. | |
| //! | |
| //! Exception: to avoid OS free/alloc churn when right on the edge, the heap | |
| //! will try to hold onto one entirely-free, non-large OS allocation instead of | |
| //! returning it to the OS. See `cached_os_alloc`. | |
| use core::cell::UnsafeCell; | |
| use core::cmp::{max, min}; | |
| use core::ffi::c_void; | |
| use core::mem::{align_of, size_of}; | |
| use core::ptr; | |
| use core::sync::atomic::{AtomicUsize, Ordering}; | |
| use bitflags::bitflags; | |
| use ksync::lock; | |
| pub(crate) use platform::*; | |
| #[cfg(not(feature = "userspace"))] | |
| mod platform { | |
| pub use crate::vm::heap::heap_wrapper::{ | |
| heap_page_alloc, heap_page_free, heap_report_alloc_failure, | |
| }; | |
| pub(crate) use debug::{dprintf, ltrace_entry, ltracef}; | |
| #[cfg(feature = "heap_enable_tests")] | |
| pub(crate) use kprint::kprintln; | |
| pub(crate) const LOCAL_TRACE: u32 = 0; | |
| macro_rules! local_trace_duration { | |
| ($label:tt, $name:ident $(, $key:tt => $val:expr)*) => { | |
| let $name = { | |
| if false { | |
| let _ = ($label $(, $key, $val)*); | |
| } | |
| }; | |
| }; | |
| } | |
| pub(crate) use local_trace_duration; | |
| macro_rules! local_trace_duration_end { | |
| ($name:ident) => { | |
| let _ = $name; | |
| }; | |
| } | |
| pub(crate) use local_trace_duration_end; | |
| macro_rules! ktrace_duration_begin { | |
| ($category:expr, $label:expr $(, $key:expr => $val:expr)*) => { | |
| $crate::ktrace_rs::duration_begin!($category, $label $(, $key => $val)*); | |
| }; | |
| } | |
| pub(crate) use ktrace_duration_begin; | |
| macro_rules! ktrace_duration_end { | |
| ($category:expr, $label:expr $(, $key:expr => $val:expr)*) => { | |
| $crate::ktrace_rs::duration_end!($category, $label $(, $key => $val)*); | |
| }; | |
| } | |
| pub(crate) use ktrace_duration_end; | |
| crate::ksync_kernel::declare_singleton_critical_mutex!( | |
| /// This is a popular lock. Critical sections protected by this lock are | |
| /// *usually* short-lived, so we'd like to minimize lock thrash by disabling | |
| /// preemption while holding the lock. However, in degenerate cases (e.g. with | |
| /// excessive PMM fragmentation), the lock can be held for long periods of time. | |
| /// In those cases, we don't want to overly delay the scheduling of a higher | |
| /// priority task. To strike a balance between these competing concerns, we use | |
| /// a critical mutex which employs "time-limited preemption deferral". | |
| /// | |
| /// For more information, see https://fxbug.dev/399694236. | |
| pub(crate) TheHeapLock | |
| ); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_64, "malloc.size_le_64", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_96, "malloc.size_le_96", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_128, "malloc.size_le_128", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_256, "malloc.size_le_256", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_384, "malloc.size_le_384", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_512, "malloc.size_le_512", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_1024, "malloc.size_le_1024", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_LE_2048, "malloc.size_le_2048", Sum); | |
| crate::counters::define_kcounter!(MALLOC_SIZE_OTHER, "malloc.size_other", Sum); | |
| // The number of failed attempts at growing the heap. | |
| crate::counters::define_kcounter!(MALLOC_HEAP_GROW_FAIL, "malloc.heap_grow_fail", Sum); | |
| #[inline] | |
| pub(crate) fn record_malloc_size(size: usize) { | |
| let counter = if size <= 64 { | |
| &MALLOC_SIZE_LE_64 | |
| } else if size <= 96 { | |
| &MALLOC_SIZE_LE_96 | |
| } else if size <= 128 { | |
| &MALLOC_SIZE_LE_128 | |
| } else if size <= 256 { | |
| &MALLOC_SIZE_LE_256 | |
| } else if size <= 384 { | |
| &MALLOC_SIZE_LE_384 | |
| } else if size <= 512 { | |
| &MALLOC_SIZE_LE_512 | |
| } else if size <= 1024 { | |
| &MALLOC_SIZE_LE_1024 | |
| } else if size <= 2048 { | |
| &MALLOC_SIZE_LE_2048 | |
| } else { | |
| &MALLOC_SIZE_OTHER | |
| }; | |
| counter.add(1); | |
| } | |
| #[inline] | |
| pub(crate) fn record_heap_grow_fail() { | |
| MALLOC_HEAP_GROW_FAIL.add(1); | |
| } | |
| } | |
| #[cfg(feature = "userspace")] | |
| mod platform { | |
| pub use crate::cmpctmalloc_ffi::{heap_page_alloc, heap_page_free, heap_report_alloc_failure}; | |
| // When built in a host (aka non-kernel) environment it is assumed to be for testing and so we | |
| // want to make sure that debug assertions are correctly enabled. This is to prevent any | |
| // regressions due to libraries and build system changes. | |
| zr::static_assert!(cfg!(debug_assertions), "Expect debug assertions in host builds"); | |
| macro_rules! dprintf { | |
| ($level:ident, $($arg:tt)*) => { | |
| let _ = format_args!($($arg)*); | |
| }; | |
| } | |
| pub(crate) use dprintf; | |
| macro_rules! ltracef { | |
| ($($arg:tt)*) => { | |
| let _ = format_args!($($arg)*); | |
| }; | |
| } | |
| pub(crate) use ltracef; | |
| macro_rules! ltrace_entry { | |
| () => {}; | |
| } | |
| pub(crate) use ltrace_entry; | |
| #[cfg(feature = "heap_enable_tests")] | |
| macro_rules! kprintln { | |
| ($($arg:tt)*) => { | |
| let _ = format_args!($($arg)*); | |
| }; | |
| } | |
| #[cfg(feature = "heap_enable_tests")] | |
| pub(crate) use kprintln; | |
| macro_rules! local_trace_duration { | |
| ($label:tt, $name:ident $(, $key:tt => $val:expr)*) => { | |
| let $name = { | |
| if false { | |
| let _ = ($label $(, $key, $val)*); | |
| } | |
| }; | |
| }; | |
| } | |
| pub(crate) use local_trace_duration; | |
| macro_rules! local_trace_duration_end { | |
| ($name:ident) => { | |
| let _ = $name; | |
| }; | |
| } | |
| pub(crate) use local_trace_duration_end; | |
| macro_rules! ktrace_duration_begin { | |
| ($category:expr, $label:expr $(, $key:expr => $val:expr)*) => { | |
| let _ = ($category, $label $(, $key, $val)*); | |
| }; | |
| } | |
| pub(crate) use ktrace_duration_begin; | |
| macro_rules! ktrace_duration_end { | |
| ($category:expr, $label:expr $(, $key:expr => $val:expr)*) => { | |
| let _ = ($category, $label $(, $key, $val)*); | |
| }; | |
| } | |
| pub(crate) use ktrace_duration_end; | |
| ksync::declare_singleton_mutex!(pub(crate) TheHeapLock); | |
| #[inline] | |
| pub(crate) fn record_malloc_size(_size: usize) {} | |
| #[inline] | |
| pub(crate) fn record_heap_grow_fail() {} | |
| } | |
| bitflags! { | |
| /// Options controlling `cmpct_dump` output. | |
| #[repr(transparent)] | |
| #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] | |
| pub struct CmpctDumpOptions: u8 { | |
| /// Standard dump output. | |
| const NONE = 0x0; | |
| /// Dump called at panic time; avoids blocking locks. | |
| const PANIC_TIME = 0x1; | |
| /// Include verbose free-block details. | |
| const VERBOSE = 0x2; | |
| } | |
| } | |
| zr::static_assert!(size_of::<CmpctDumpOptions>() == 1); | |
| zr::static_assert!(align_of::<CmpctDumpOptions>() == 1); | |
| use debug_fill::*; | |
| #[cfg(all(not(feature = "userspace"), any(feature = "cmpct_debug", lk_debuglevel_gt2)))] | |
| mod debug_fill { | |
| use core::ffi::c_void; | |
| use core::mem::size_of; | |
| use core::ptr; | |
| use kprint::kprintln; | |
| use super::{Free, Header}; | |
| /// TODO(https://fxbug.dev/404291515): Enable on riscv to track down heap corruption. | |
| const ALLOC_FILL: u8 = 0x99; | |
| const FREE_FILL: u8 = 0x77; | |
| const PADDING_FILL: u8 = 0x55; | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `ptr` is readable for `size` bytes. | |
| #[sanitize(address = "off")] | |
| unsafe fn hexdump8(ptr: *const c_void, size: usize) { | |
| // SAFETY: Caller guarantees `ptr` is readable for `size` bytes; `hexdump8_very_ex_raw` | |
| // avoids creating a safe Rust slice over potentially KASAN-poisoned memory. | |
| let _ = unsafe { | |
| pretty::hexdump8_very_ex_raw( | |
| &mut debug::dprintf::KernelConsoleWriter, | |
| ptr.cast::<u8>(), | |
| size, | |
| ptr.addr() as u64, | |
| ) | |
| }; | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `ptr` is readable for `size` bytes. | |
| #[sanitize(address = "off")] | |
| unsafe fn check_free_fill(ptr: *const c_void, size: usize) { | |
| // The first 16 bytes of the region won't have free fill due to overlap | |
| // with the allocator bookkeeping. | |
| // SAFETY: Caller guarantees `ptr` is readable for `size` bytes. | |
| unsafe { | |
| let start = size_of::<Free>() - size_of::<Header>(); | |
| for i in start..size { | |
| let byte = *ptr.cast::<u8>().add(i); | |
| if byte != FREE_FILL { | |
| crate::platform_rs::power::platform_panic_start(); | |
| kprintln!("Heap free fill check fail. Allocated region:"); | |
| hexdump8(ptr, size); | |
| panic!("allocating {} bytes, fill was {:02x}, offset {}", size, byte, i); | |
| } | |
| } | |
| } | |
| } | |
| /// Fills the newly created free area with the debug free pattern. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure `free_area` is valid for writes of `size` bytes. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn fill_free_area(free_area: *mut Free, size: usize) { | |
| // SAFETY: Caller guarantees `free_area` is valid for `size` bytes. | |
| unsafe { | |
| ptr::write_bytes(free_area.add(1).cast::<u8>(), FREE_FILL, size - size_of::<Free>()); | |
| } | |
| } | |
| /// Fills the newly allocated area with the debug fill pattern. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure `result` is valid for writes of | |
| /// `rounded_up - size_of::<Header>()` bytes. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn fill_alloc_area(result: *mut c_void, size: usize, rounded_up: usize) { | |
| // SAFETY: Caller guarantees `result` is valid for `rounded_up - size_of::<Header>()` bytes. | |
| unsafe { | |
| check_free_fill(result, size); | |
| ptr::write_bytes(result.cast::<u8>(), ALLOC_FILL, size); | |
| ptr::write_bytes( | |
| result.cast::<u8>().add(size), | |
| PADDING_FILL, | |
| rounded_up - size - size_of::<Header>(), | |
| ); | |
| } | |
| } | |
| #[inline] | |
| pub(super) fn on_init() { | |
| kprintln!("CMPCT: Enabling debug"); | |
| } | |
| } | |
| #[cfg(not(all(not(feature = "userspace"), any(feature = "cmpct_debug", lk_debuglevel_gt2))))] | |
| mod debug_fill { | |
| use core::ffi::c_void; | |
| use super::Free; | |
| /// Fills the newly created free area with the debug free pattern. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure `free_area` is valid for writes of `size` bytes. | |
| #[inline] | |
| pub(super) unsafe fn fill_free_area(_free_area: *mut Free, _size: usize) {} | |
| /// Fills the newly allocated area with the debug fill pattern. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure `result` is valid for writes of | |
| /// `rounded_up - size_of::<Header>()` bytes. | |
| #[inline] | |
| pub(super) unsafe fn fill_alloc_area(_result: *mut c_void, _size: usize, _rounded_up: usize) {} | |
| #[inline] | |
| pub(super) fn on_init() {} | |
| } | |
| /// `HEAP_GROW_SIZE` is minimum size by which the heap is grown. | |
| /// | |
| /// A larger value can provide some performance improvement at the cost of wasted | |
| /// memory. | |
| /// | |
| /// See also [`HEAP_LARGE_ALLOC_BYTES`]. | |
| const HEAP_GROW_SIZE: usize = 256 * 1024; | |
| zr::static_assert!(page::is_page_rounded(HEAP_GROW_SIZE)); | |
| /// `HEAP_ALLOC_VIRTUAL_BITS` defines the largest allocation bucket. | |
| /// | |
| /// The requirements on virtual bits is that the largest allocation (including | |
| /// header), must roundup to not more `2**HEAP_ALLOC_VIRTUAL_BITS` than this | |
| /// alignment, and similarly the heap cannot grow by amounts that would not round | |
| /// down to `2**HEAP_ALLOC_VIRTUAL_BITS` or less. As such the heap can grow by more | |
| /// than this many bits at once, but not so many as it must fall into the next | |
| /// bucket. | |
| const HEAP_ALLOC_VIRTUAL_BITS: usize = 21; | |
| /// `HEAP_LARGE_ALLOC_BYTES` limits size of any single allocation. | |
| /// | |
| /// A larger value will, on average, "waste" more memory. Why is that? When | |
| /// freeing memory the heap may hold on to a block before returning it to the | |
| /// underlying allocator (see `THE_HEAP.cached_os_alloc`). The size of the cached | |
| /// block is limited by `HEAP_LARGE_ALLOC_BYTES` so reducing this value limits the | |
| /// size of the cached block. | |
| /// | |
| /// Note that `HEAP_LARGE_ALLOC_BYTES` is the largest internal allocation that the | |
| /// heap can do, and includes any headers. The largest allocation `cmpct_alloc` | |
| /// could theoretically (it may be artificially limited) provide is therefore | |
| /// slightly less than this. | |
| /// | |
| /// See also [`HEAP_GROW_SIZE`]. | |
| const HEAP_LARGE_ALLOC_BYTES: usize = (1usize << HEAP_ALLOC_VIRTUAL_BITS) - HEAP_GROW_OVERHEAD; | |
| /// Buckets for allocations. The smallest 15 buckets are 8, 16, 24, etc. up to | |
| /// 120 bytes. After that we round up to the nearest size that can be written | |
| /// `/^0*1...0*$/`, giving 8 buckets per order of binary magnitude. The freelist | |
| /// entries in a given bucket have at least the given size, plus the header | |
| /// size. On 64 bit, the 8 byte bucket is useless, since the freelist header | |
| /// is 16 bytes larger than the header, but we have it for simplicity. | |
| const NUMBER_OF_BUCKETS: usize = 1 + 15 + (HEAP_ALLOC_VIRTUAL_BITS - 7) * 8; | |
| /// If a header's `left` field has this bit set, it is free and lives in | |
| /// a free bucket. | |
| const FREE_BIT: usize = 1 << 0; | |
| const HEADER_LEFT_BIT_MASK: usize = FREE_BIT; | |
| /// All individual memory areas on the heap start with this. | |
| #[repr(C)] | |
| struct Header { | |
| /// Pointer to the previous area in memory order. The lower bit is used | |
| /// to store extra state: see [`FREE_BIT`]. The left sentinel will have | |
| /// NULL in the address portion of this field. Left and right sentinels | |
| /// will always be marked as "allocated" to avoid coalescing. | |
| // Safety invariant: `untag(left)` is either null (for the left sentinel of an OS allocation) | |
| // or points to the preceding `Header` within the same OS allocation; bit 0 (`FREE_BIT`) is | |
| // set iff this memory area is currently free and linked in `THE_HEAP.free_lists`. | |
| left: *mut Header, | |
| /// The size of the memory area in bytes, including this header. | |
| /// The right sentinel will have 0 in this field. | |
| /// A 32-bit value is used for the size to leave room for the cookie, and the | |
| /// maximum size of our allowed allocations fits easily within this. | |
| // Safety invariant: Either `0` (for the right sentinel of an OS allocation) or the byte size | |
| // (`>= size_of::<Header>()`, 8-byte aligned) extending to the adjacent right `Header` within | |
| // the same OS allocation (or the total page-aligned OS allocation size when stored in | |
| // `THE_HEAP.cached_os_alloc`). | |
| size: u32, | |
| /// User defined cookie that can be associated with an allocation. | |
| cookie: u32, | |
| } | |
| zr::static_assert!(size_of::<Header>() == 16); | |
| zr::static_assert!(align_of::<Header>() == 8); | |
| /// When the heap is grown the requested internal usable size will be increased | |
| /// by this amount before allocating from the OS. This can be factored into | |
| /// any `heap_grow` requested to precisely control the OS allocation amount. | |
| const HEAP_GROW_OVERHEAD: usize = size_of::<Header>() * 2; | |
| /// Precalculated version of [`HEAP_GROW_SIZE`] that takes into account the grow | |
| /// overhead. | |
| const HEAP_USABLE_GROW_SIZE: usize = HEAP_GROW_SIZE - HEAP_GROW_OVERHEAD; | |
| // When we grow the heap we have to have somewhere in the freelist to put the | |
| // resulting freelist entry, so the freelist has to have a certain number of | |
| // buckets. | |
| zr::static_assert!(HEAP_GROW_SIZE <= HEAP_LARGE_ALLOC_BYTES); | |
| // We use a 32-byte value to store sizes, so ensure the largest allocation fits | |
| // inside such a value. | |
| zr::static_assert!(HEAP_LARGE_ALLOC_BYTES < u32::MAX as usize); | |
| #[repr(C)] | |
| struct Free { | |
| // Safety invariant: `header.size as usize >= size_of::<Free>()` and `header.left` has | |
| // `FREE_BIT` set while this node is in a free list. | |
| header: Header, | |
| // Safety invariant: Either null or points to the next valid `Free` node in the same bucket's | |
| // doubly-linked list under `TheHeapLock`. | |
| next: *mut Free, | |
| // Safety invariant: Either null (when at the head of the bucket) or points to the previous | |
| // valid `Free` node in the same bucket's doubly-linked list under `TheHeapLock`. | |
| prev: *mut Free, | |
| } | |
| zr::static_assert!(size_of::<Free>() == 32); | |
| zr::static_assert!(align_of::<Free>() == 8); | |
| /// Bitmask that tracks whether a given `free_lists` entry has any elements. | |
| /// See [`set_free_list_bit`], [`clear_free_list_bit`]. | |
| const BUCKET_WORDS: usize = (NUMBER_OF_BUCKETS + 31) >> 5; | |
| #[cfg_attr(not(feature = "userspace"), cfg(not(sanitize = "address")))] | |
| mod asan_stub { | |
| use core::ffi::c_void; | |
| use super::{Header, Heap}; | |
| pub(super) struct Quarantine; | |
| impl Quarantine { | |
| pub(super) const fn new() -> Self { | |
| Self | |
| } | |
| /// Pushes `header` into the ASAN quarantine (no-op when ASAN is disabled). | |
| #[inline] | |
| pub(super) fn push(&mut self, header: *mut Header) -> *mut Header { | |
| header | |
| } | |
| } | |
| #[inline] | |
| pub(super) const fn adjust_alloc_size(size: usize, _alloc_size: usize) -> usize { | |
| size | |
| } | |
| /// Poisons the allocation's header and trailing right redzone, while unpoisoning its usable | |
| /// payload (no-op when ASAN is disabled). | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that the header and redzone regions are backed by KASAN shadow | |
| /// memory. | |
| #[inline] | |
| pub(super) unsafe fn poison_allocation( | |
| _head_addr: usize, | |
| _payload_addr: usize, | |
| _alloc_size: usize, | |
| ) { | |
| } | |
| /// Poisons the `memalign` allocation and its padding (no-op when ASAN is disabled). | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `[unaligned_addr, unaligned_addr + padded_size)` is backed by | |
| /// KASAN shadow memory. | |
| #[inline] | |
| pub(super) unsafe fn poison_memalign_padding(_unaligned_addr: usize, _padded_size: usize) {} | |
| /// Unpoisons the `memalign` allocation payload (no-op when ASAN is disabled). | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `[payload_addr, payload_addr + size)` is backed by KASAN | |
| /// shadow memory. | |
| #[inline] | |
| pub(super) unsafe fn unpoison_memalign_payload(_payload_addr: usize, _size: usize) {} | |
| /// Quarantines the freed allocation if quarantine is enabled (returns `header` directly when | |
| /// ASAN is disabled). | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `header` is a valid allocated | |
| /// `Header`. | |
| #[inline] | |
| pub(super) unsafe fn quarantine_or_free( | |
| theheap: *mut Heap, | |
| _payload: *mut c_void, | |
| header: *mut Header, | |
| _header_size: usize, | |
| ) -> *mut Header { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `theheap` is valid. | |
| unsafe { (*theheap).asan_quarantine.push(header) } | |
| } | |
| } | |
| #[cfg(not(feature = "userspace"))] | |
| #[cfg(sanitize = "address")] | |
| mod kernel_asan { | |
| use core::ffi::c_void; | |
| use core::mem::size_of; | |
| use super::{HEAP_LARGE_ALLOC_BYTES, HEAP_MAX_ALLOC_SIZE, Header, Heap, SizeToIndexRet}; | |
| use crate::instrumentation::asan::{ | |
| ASAN_HEAP_FREE_MAGIC, ASAN_HEAP_LEFT_REDZONE_MAGIC, Quarantine as AsanQuarantine, | |
| asan_heap_redzone_size, asan_poison_shadow, asan_unpoison_shadow, | |
| }; | |
| // Safety invariant: Every non-null element stored in `THE_HEAP`'s quarantine is a valid, | |
| // quarantined `Header` pointer within an OS allocation that has not yet been returned to a | |
| // free list or the OS. | |
| pub(super) type Quarantine = AsanQuarantine<Header>; | |
| /// Adds space at the end of the allocation for an ASAN right redzone. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) fn adjust_alloc_size(size: usize, alloc_size: usize) -> usize { | |
| debug_assert!(alloc_size <= HEAP_MAX_ALLOC_SIZE); | |
| // Add space at the end of the allocation for a redzone. | |
| // A redzone is used to detect buffer overflows by oversizing the buffer and poisoning | |
| // the excess memory. The redzone is after the buffer - before the buffer is a | |
| // Header, which is also poisoned. | |
| // SAFETY: `alloc_size <= HEAP_MAX_ALLOC_SIZE`, so `asan_heap_redzone_size` cannot | |
| // overflow. | |
| let (redzone, max_redzone) = unsafe { | |
| (asan_heap_redzone_size(alloc_size), asan_heap_redzone_size(HEAP_MAX_ALLOC_SIZE)) | |
| }; | |
| let size = size + redzone; | |
| // When we validated the max allocation size above, we did not take into account the | |
| // asan redzone. Unfortunately this cannot presently be checked statically due to the | |
| // asan_heap_redzone_size not being a constexpr, and so we check it here instead. | |
| assert!( | |
| SizeToIndexRet::size_to_index_allocating(max_redzone + HEAP_MAX_ALLOC_SIZE).rounded_up | |
| + size_of::<Header>() | |
| <= HEAP_LARGE_ALLOC_BYTES | |
| ); | |
| size | |
| } | |
| /// Poisons the allocation's header and trailing right redzone, while | |
| /// unpoisoning its usable payload. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `head` and `redzone` regions are backed by | |
| /// KASAN shadow memory. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn poison_allocation( | |
| head_addr: usize, | |
| payload_addr: usize, | |
| alloc_size: usize, | |
| ) { | |
| let redzone_start = payload_addr + alloc_size; | |
| // SAFETY: Caller guarantees the header, payload, and redzone regions are backed by KASAN | |
| // shadow memory, and `alloc_size <= HEAP_MAX_ALLOC_SIZE`. | |
| unsafe { | |
| asan_poison_shadow(head_addr, size_of::<Header>(), ASAN_HEAP_LEFT_REDZONE_MAGIC); | |
| let redzone_size = asan_heap_redzone_size(alloc_size); | |
| asan_poison_shadow(redzone_start, redzone_size, ASAN_HEAP_LEFT_REDZONE_MAGIC); | |
| asan_unpoison_shadow(payload_addr, alloc_size); | |
| } | |
| } | |
| /// Poisons the memalign allocation and its padding. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `[unaligned_addr, unaligned_addr + padded_size)` | |
| /// is backed by KASAN shadow memory. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn poison_memalign_padding(unaligned_addr: usize, padded_size: usize) { | |
| // TODO(https://fxbug.dev/42104852): Separately poison padding and the post-buffer | |
| // redzone. | |
| // SAFETY: Caller guarantees `[unaligned_addr, unaligned_addr + padded_size)` is backed by | |
| // KASAN shadow memory. | |
| unsafe { | |
| asan_poison_shadow(unaligned_addr, padded_size, ASAN_HEAP_LEFT_REDZONE_MAGIC); | |
| } | |
| } | |
| /// Unpoisons the memalign allocation payload. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `[payload_addr, payload_addr + size)` is backed | |
| /// by KASAN shadow memory. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn unpoison_memalign_payload(payload_addr: usize, size: usize) { | |
| // SAFETY: Caller guarantees `[payload_addr, payload_addr + size)` is backed by KASAN | |
| // shadow memory. | |
| unsafe { | |
| asan_unpoison_shadow(payload_addr, size); | |
| } | |
| } | |
| /// Quarantines the freed allocation if quarantine is enabled. Returns null if quarantined, | |
| /// or the oldest evicted allocation `Header` if the quarantine was full. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `header` is a valid | |
| /// allocated `Header`. | |
| #[sanitize(address = "off")] | |
| #[inline] | |
| pub(super) unsafe fn quarantine_or_free( | |
| theheap: *mut Heap, | |
| payload: *mut c_void, | |
| header: *mut Header, | |
| header_size: usize, | |
| ) -> *mut Header { | |
| // SAFETY: Caller holds `TheHeapLock` and `header` is a valid allocated `Header` whose | |
| // payload is backed by KASAN shadow memory. | |
| unsafe { | |
| asan_poison_shadow( | |
| payload.addr(), | |
| header_size - size_of::<Header>(), | |
| ASAN_HEAP_FREE_MAGIC, | |
| ); | |
| (*theheap).asan_quarantine.push(header) | |
| } | |
| } | |
| } | |
| #[cfg(not(feature = "userspace"))] | |
| #[cfg(sanitize = "address")] | |
| use kernel_asan as asan; | |
| #[cfg_attr(not(feature = "userspace"), cfg(not(sanitize = "address")))] | |
| use asan_stub as asan; | |
| use asan::Quarantine; | |
| struct Heap { | |
| /// Total bytes allocated from the OS for the heap. | |
| size: usize, | |
| /// Bytes of usable free space in the heap. | |
| // Safety invariant: Equals the sum of `(*free_area).header.size` across all nodes in | |
| // `free_lists`. | |
| remaining: usize, | |
| /// A non-large OS allocation that could have been freed to the OS but | |
| /// wasn't. We will attempt to use this before allocating more memory from | |
| /// the OS, to reduce churn. May be null. If non-null, `cached_os_alloc.size` | |
| /// holds the total size allocated from the OS for this block. | |
| // Safety invariant: Either null or points to a page-aligned OS allocation of | |
| // `(*cached_os_alloc).size` page-rounded bytes (`>= size_of::<Header>()`). | |
| cached_os_alloc: *mut Header, | |
| /// Free lists, bucketed by size. See [`size_to_index_helper`]. | |
| // Safety invariant: Each `free_lists[i]` is either null or the head of a valid doubly-linked | |
| // list of `Free` nodes whose usable sizes map to bucket `i`. | |
| free_lists: [*mut Free; NUMBER_OF_BUCKETS], | |
| /// Bitmask that tracks whether a given `free_lists` entry has any elements. | |
| /// See [`set_free_list_bit`], [`clear_free_list_bit`]. | |
| // Safety invariant: Bit `31 - (i & 0x1f)` of `free_list_bits[i >> 5]` is 1 iff | |
| // `free_lists[i]` is non-null. | |
| free_list_bits: [u32; BUCKET_WORDS], | |
| asan_quarantine: Quarantine, | |
| } | |
| impl Heap { | |
| const fn new() -> Self { | |
| Self { | |
| size: 0, | |
| remaining: 0, | |
| cached_os_alloc: ptr::null_mut(), | |
| free_lists: [ptr::null_mut(); NUMBER_OF_BUCKETS], | |
| free_list_bits: [0; BUCKET_WORDS], | |
| asan_quarantine: Quarantine::new(), | |
| } | |
| } | |
| } | |
| struct SyncHeap(UnsafeCell<Heap>); | |
| // SAFETY: Access to `THE_HEAP` is serialized by `TheHeapLock` (except during panic-time | |
| // `cmpct_dump`, when all other CPUs are halted). | |
| unsafe impl Sync for SyncHeap {} | |
| impl SyncHeap { | |
| const fn new() -> Self { | |
| Self(UnsafeCell::new(Heap::new())) | |
| } | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| const fn get(&self) -> *mut Heap { | |
| self.0.get() | |
| } | |
| } | |
| /// Global `cmpctmalloc` heap state protected by [`TheHeapLock`]. | |
| static THE_HEAP: SyncHeap = SyncHeap::new(); | |
| /// Maximum allocation size automatically zero-filled on allocation. | |
| /// | |
| /// `AtomicUsize` is used because `FILL_ON_ALLOC_THRESHOLD` is read (in `cmpct_alloc` after | |
| /// releasing `TheHeapLock`) and written (in `cmpct_set_fill_on_alloc_threshold`) without holding | |
| /// `TheHeapLock`. `Ordering::Relaxed` is sufficient for all accesses because it is an independent | |
| /// configuration threshold with no dependent memory operations to synchronize. | |
| static FILL_ON_ALLOC_THRESHOLD: AtomicUsize = AtomicUsize::new(0); | |
| /// Asserts that `$cond` is true in debug builds and hints to the optimizer that it is | |
| /// always true in release builds. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must guarantee that `$cond` holds. Violating this is immediate undefined | |
| /// behavior in release builds. | |
| macro_rules! assume { | |
| ($cond:expr $(,)?) => {{ | |
| let cond: bool = $cond; | |
| debug_assert!(cond, "assertion failed: {}", stringify!($cond)); | |
| core::hint::assert_unchecked(cond); | |
| }}; | |
| } | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| const fn round_up(val: usize, align: usize) -> usize { | |
| (val + align - 1) & !(align - 1) | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held (or all other CPUs are halted in a kernel | |
| /// panic with no concurrent mutation of `THE_HEAP`) and `header` points to a valid `Header`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| unsafe fn dump_free(header: *mut Header) { | |
| // This function accesses header->size so it has to be NO_ASAN | |
| // SAFETY: Caller guarantees `header` is a valid `Header` pointer. | |
| unsafe { | |
| let size = (*header).size; | |
| dprintf!( | |
| INFO, | |
| "\t\tbase {:p}, end {:#x}, len {:#x} ({})\n", | |
| header, | |
| header.addr() + (size as usize), | |
| size, | |
| size | |
| ); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that either `TheHeapLock` is held or all other CPUs are halted in a | |
| /// kernel panic with no concurrent mutation of `THE_HEAP`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| unsafe fn cmpct_dump_locked(options: CmpctDumpOptions) { | |
| // This function accesses Free * that are poisoned so it has to be NO_ASAN | |
| // SAFETY: Caller guarantees `TheHeapLock` is held (or all other CPUs are halted in a kernel | |
| // panic with no concurrent mutation of `THE_HEAP`) and all nodes on `free_lists` are valid | |
| // `Free` pointers. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| dprintf!(INFO, "Heap dump (using cmpctmalloc):\n"); | |
| dprintf!( | |
| INFO, | |
| "\tsize {}, remaining {}, (used {}) cached free {}\n", | |
| (*theheap).size, | |
| (*theheap).remaining, | |
| (*theheap).size - (*theheap).remaining, | |
| if !(*theheap).cached_os_alloc.is_null() { | |
| (*(*theheap).cached_os_alloc).size | |
| } else { | |
| 0 | |
| } | |
| ); | |
| if options.contains(CmpctDumpOptions::VERBOSE) { | |
| dprintf!(INFO, "\tfree list:\n"); | |
| for i in 0..NUMBER_OF_BUCKETS { | |
| let mut header_printed = false; | |
| let mut free_area = (*theheap).free_lists[i]; | |
| while !free_area.is_null() { | |
| assert!(free_area != (*free_area).next); | |
| if !header_printed { | |
| dprintf!(INFO, "\tbucket {}\n", i); | |
| header_printed = true; | |
| } | |
| dump_free(&raw mut (*free_area).header); | |
| free_area = (*free_area).next; | |
| } | |
| } | |
| } else { | |
| dprintf!(INFO, "Verbose options not passed. Skipping free list dump\n"); | |
| } | |
| } | |
| } | |
| #[derive(Copy, Clone)] | |
| struct SizeToIndexRet { | |
| bucket: i32, | |
| rounded_up: usize, | |
| } | |
| /// Operates in sizes that don't include the allocation header; | |
| /// i.e., the usable portion of a memory area. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| const fn size_to_index_helper(mut size: usize, adjust: isize, increment: i32) -> SizeToIndexRet { | |
| let rounded_up: usize; | |
| // First buckets are simply 8-spaced up to 128. | |
| if size <= 128 { | |
| if size_of::<usize>() == 8 && size <= size_of::<Free>() - size_of::<Header>() { | |
| rounded_up = size_of::<Free>() - size_of::<Header>(); | |
| } else { | |
| rounded_up = size; | |
| } | |
| // No allocation is smaller than 8 bytes, so the first bucket is for 8 | |
| // byte spaces (not including the header). For 64 bit, the free list | |
| // struct is 16 bytes larger than the header, so no allocation can be | |
| // smaller than that (otherwise how to free it), but we have empty 8 | |
| // and 16 byte buckets for simplicity. | |
| let shifted = if (size >> 3) >= 1 { size >> 3 } else { 1 }; | |
| return SizeToIndexRet { bucket: (shifted as i32) - 1, rounded_up }; | |
| } | |
| // We are going to go up to the next size to round up, but if we hit a | |
| // bucket size exactly we don't want to go up. By subtracting 8 here, we | |
| // will do the right thing (the carry propagates up for the round numbers | |
| // we are interested in). | |
| size = size.wrapping_add_signed(adjust); | |
| // After 128 the buckets are logarithmically spaced, every 16 up to 256, | |
| // every 32 up to 512 etc. This can be thought of as rows of 8 buckets. | |
| // GCC intrinsic count-leading-zeros. | |
| // Eg. 128-255 has 24 leading zeros and we want row to be 4. | |
| let row: u32 = (size_of::<usize>() as u32) * 8 - 4 - size.leading_zeros(); | |
| // For row 4 we want to shift down 4 bits. | |
| let column: u32 = ((size >> row) & 7) as u32; | |
| let mut row_column: i32 = ((row << 3) | column) as i32; | |
| row_column += increment; | |
| size = (8 + ((row_column & 7) as usize)) << (row_column >> 3); | |
| rounded_up = size; | |
| // We start with 15 buckets, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, | |
| // 104, 112, 120. Then we have row 4, sizes 128 and up, with the | |
| // row-column 8 and up. | |
| let answer: i32 = row_column + 15 - 32; | |
| debug_assert!((answer as usize) < NUMBER_OF_BUCKETS); | |
| SizeToIndexRet { bucket: answer, rounded_up } | |
| } | |
| impl SizeToIndexRet { | |
| /// Round up size to next bucket when allocating. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| const fn size_to_index_allocating(size: usize) -> SizeToIndexRet { | |
| let rounded = round_up(size, 8); | |
| size_to_index_helper(rounded, -8, 1) | |
| } | |
| } | |
| /// Round down size to next bucket when freeing. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| const fn size_to_index_freeing(size: usize) -> i32 { | |
| size_to_index_helper(size, 0, 0).bucket | |
| } | |
| // Ensure that HEAP_LARGE_ALLOC_BYTES maps to a valid bucket when allocating. | |
| // Given how HEAP_LARGE_ALLOC_BYTES is defined this assert is somewhat excessive | |
| // but included for completeness should HEAP_LARGE_ALLOC_BYTES ever be given a | |
| // more complicated definition. Note that HEAP_LARGE_ALLOC_BYTES is the internal | |
| // size (including header), but we map to a bucket using the size without the | |
| // header, as the header is implicitly included in the final bucket. Or put | |
| // another way, the bucket for size X gives a bucket with a free block of | |
| // X+size_of::<Header>(). | |
| zr::static_assert!( | |
| (SizeToIndexRet::size_to_index_allocating(HEAP_LARGE_ALLOC_BYTES - size_of::<Header>()).bucket | |
| as usize) | |
| <= NUMBER_OF_BUCKETS | |
| ); | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| fn tag_as_free(left: *mut c_void) -> *mut Header { | |
| left.map_addr(|addr| addr | FREE_BIT).cast::<Header>() | |
| } | |
| /// Returns true if this `Header` is marked as free. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `header` points to a valid `Header`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn is_tagged_as_free(header: *const Header) -> bool { | |
| // The free bit is stashed in the lower bit of header->left. | |
| // SAFETY: Caller guarantees `header` is a valid `Header` pointer. | |
| unsafe { ((*header).left.addr() & FREE_BIT) != 0 } | |
| } | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| fn untag(left: *const c_void) -> *mut Header { | |
| left.cast_mut().map_addr(|addr| addr & !HEADER_LEFT_BIT_MASK).cast::<Header>() | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `header` points to a valid `Header` within | |
| /// an OS allocation, and `header.cast::<u8>().add((*header).size as usize)` points to the adjacent | |
| /// right `Header` within the same OS allocation. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn right_header(header: *mut Header) -> *mut Header { | |
| // SAFETY: Caller guarantees `header` is a valid `Header` and `header + header->size` points to | |
| // the adjacent `Header` within the same OS allocation. | |
| unsafe { header.cast::<u8>().add((*header).size as usize).cast::<Header>() } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn set_free_list_bit(index: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS` | |
| // (`index >> 5 < BUCKET_WORDS`). | |
| unsafe { | |
| assume!(index < NUMBER_OF_BUCKETS); | |
| let theheap = THE_HEAP.get(); | |
| (*theheap).free_list_bits[index >> 5] |= 1u32 << (31 - (index & 0x1f)); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn clear_free_list_bit(index: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS` | |
| // (`index >> 5 < BUCKET_WORDS`). | |
| unsafe { | |
| assume!(index < NUMBER_OF_BUCKETS); | |
| let theheap = THE_HEAP.get(); | |
| (*theheap).free_list_bits[index >> 5] &= !(1u32 << (31 - (index & 0x1f))); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn find_nonempty_bucket(mut index: usize) -> i32 { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `index < NUMBER_OF_BUCKETS`. | |
| unsafe { | |
| assume!(index < NUMBER_OF_BUCKETS); | |
| let theheap = THE_HEAP.get(); | |
| let mut mask: u32 = (1u32 << (31 - (index & 0x1f))) - 1; | |
| mask = mask.wrapping_mul(2).wrapping_add(1); | |
| mask &= (*theheap).free_list_bits[index >> 5]; | |
| if mask != 0 { | |
| return ((index & !0x1f) as i32) + (mask.leading_zeros() as i32); | |
| } | |
| index = round_up(index + 1, 32); | |
| while index < NUMBER_OF_BUCKETS { | |
| mask = (*theheap).free_list_bits[index >> 5]; | |
| if mask != 0 { | |
| return (index as i32) + (mask.leading_zeros() as i32); | |
| } | |
| index += 32; | |
| } | |
| -1 | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `header` points to a valid `Header`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn is_start_of_os_allocation(header: *const Header) -> bool { | |
| // SAFETY: Caller guarantees `header` is a valid `Header` pointer. | |
| unsafe { untag((*header).left.cast()) == untag(ptr::null()) } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `address` is 8-byte aligned | |
| /// (`align_of::<Free>()`) and points to a writable region of at least `size` bytes within an OS | |
| /// allocation, `size >= size_of::<Free>()`, and `size - size_of::<Header>()` maps via | |
| /// `size_to_index_freeing` to a bucket index `< NUMBER_OF_BUCKETS`. | |
| #[cfg_attr( | |
| not(feature = "userspace"), | |
| expect(inline_no_sanitize, reason = "all callers are also sanitize(address = \"off\")"), | |
| sanitize(address = "off") | |
| )] | |
| #[inline(always)] | |
| unsafe fn create_free_area(address: *mut c_void, left: *mut c_void, size: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held, `address` is 8-byte aligned and valid for | |
| // `size >= size_of::<Free>()` bytes, and `index < NUMBER_OF_BUCKETS`. | |
| unsafe { | |
| assume!(size >= size_of::<Free>()); | |
| let theheap = THE_HEAP.get(); | |
| let free_area = address.cast::<Free>(); | |
| (*free_area).header.size = size as u32; | |
| (*free_area).header.cookie = 0; | |
| (*free_area).header.left = tag_as_free(left); | |
| let index = size_to_index_freeing(size - size_of::<Header>()) as usize; | |
| assume!(index < NUMBER_OF_BUCKETS); | |
| set_free_list_bit(index); | |
| let bucket = &raw mut (*theheap).free_lists[index]; | |
| let old_head = *bucket; | |
| if !old_head.is_null() { | |
| (*old_head).prev = free_area; | |
| } | |
| (*free_area).next = old_head; | |
| (*free_area).prev = ptr::null_mut(); | |
| (*bucket) = free_area; | |
| (*theheap).remaining += size; | |
| fill_free_area(free_area, size); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `address` points to a valid `Header`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn is_end_of_os_allocation(address: *mut u8) -> bool { | |
| // SAFETY: Caller guarantees `address` points to a valid `Header`. | |
| unsafe { (*address.cast::<Header>()).size == 0 } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `ptr` is page-aligned and was allocated by | |
| /// `heap_page_alloc`, and `size` is page-aligned. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn free_to_os(ptr: *mut c_void, size: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `(ptr, size)` is a valid OS allocation. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| debug_assert!(page::is_page_rounded(ptr.addr())); | |
| debug_assert!(page::is_page_rounded(size)); | |
| heap_page_free(ptr, size >> page::SHIFT); | |
| (*theheap).size -= size; | |
| } | |
| } | |
| /// May call `free_to_os()`, or may cache the (non-large) OS allocation in | |
| /// `cached_os_alloc`. `left_sentinel` is the start of the OS allocation, and | |
| /// `total_size` is the (page-aligned) number of bytes that were originally | |
| /// allocated from the OS. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `left_sentinel` points to the start of a | |
| /// page-aligned OS allocation of `total_size` bytes (`total_size <= u32::MAX as usize`). | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn possibly_free_to_os(left_sentinel: *mut Header, total_size: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `left_sentinel` is a valid OS allocation | |
| // of `total_size` bytes. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| if (*theheap).cached_os_alloc.is_null() { | |
| ltracef!("Keeping {:#x}-byte OS alloc @{:p}\n", total_size, left_sentinel); | |
| (*left_sentinel).left = ptr::null_mut(); | |
| (*left_sentinel).size = total_size as u32; | |
| (*left_sentinel).cookie = 0; | |
| (*theheap).cached_os_alloc = left_sentinel; | |
| } else { | |
| ltracef!("Returning {:#x} bytes @{:p} to OS\n", total_size, left_sentinel); | |
| free_to_os(left_sentinel.cast(), total_size); | |
| } | |
| } | |
| } | |
| #[cold] | |
| #[inline(never)] | |
| fn panic_unexpected_left_sentinel_size(left_size: u32) -> ! { | |
| panic!("Unexpected left sentinel size {} != header size {}", left_size, size_of::<Header>()); | |
| } | |
| #[cold] | |
| #[inline(never)] | |
| fn panic_remaining_underflow(remaining: usize, free_size: u32) -> ! { | |
| panic!("{} >= {}", remaining, free_size); | |
| } | |
| #[cold] | |
| #[inline(never)] | |
| fn panic_unrounded_os_alloc(size: usize, ptr: *mut c_void) -> ! { | |
| panic!("{:#x} bytes @{:p}", size, ptr); | |
| } | |
| #[cold] | |
| #[inline(never)] | |
| fn panic_header_size_too_small(header_size: u32) -> ! { | |
| panic!("got {} min {}", header_size, size_of::<Header>()); | |
| } | |
| #[cold] | |
| #[inline(never)] | |
| fn panic_sized_free_too_small(header_size: u32, s: usize) -> ! { | |
| panic!("expected {} got {}", header_size, s); | |
| } | |
| /// Frees `size` bytes starting at `address`, either to a free bucket or to the | |
| /// OS (in which case the left/right sentinels are freed as well). `address` | |
| /// should point to what would be the `Header` of the memory area to free, and | |
| /// `left` and `size` should be set to the values that the `Header` would have | |
| /// contained. This is broken out because the `Header` will not contain the | |
| /// proper size when coalescing neighboring areas. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `address` is 8-byte aligned and points to a | |
| /// writable region of `size` bytes (`size >= size_of::<Free>()`) within an OS allocation whose | |
| /// valid adjacent right `Header` is at `address.cast::<u8>().add(size)`, `untag(left)` points to | |
| /// the valid preceding `Header` in the same OS allocation, and `size - size_of::<Header>()` maps | |
| /// via `size_to_index_freeing` to a bucket `< NUMBER_OF_BUCKETS`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn free_memory(address: *mut c_void, left: *mut c_void, size: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `address`, `left`, and `size` describe a | |
| // valid memory area within an OS allocation. | |
| unsafe { | |
| let left = untag(left); | |
| if page::is_page_rounded(left.addr()) | |
| && is_start_of_os_allocation(left) | |
| && is_end_of_os_allocation(address.cast::<u8>().add(size)) | |
| { | |
| // Assert that it's safe to do a simple 2*size_of::<Header>() below. | |
| if cfg!(debug_assertions) && ((*left).size as usize) != size_of::<Header>() { | |
| panic_unexpected_left_sentinel_size((*left).size); | |
| } | |
| possibly_free_to_os(left, size + 2 * size_of::<Header>()); | |
| } else { | |
| create_free_area(address, left.cast(), size); | |
| } | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `0 <= bucket < NUMBER_OF_BUCKETS as i32`, | |
| /// and `free_area` is a valid `Free` node currently linked in `theheap.free_lists[bucket]`. | |
| #[cfg_attr( | |
| not(feature = "userspace"), | |
| expect(inline_no_sanitize, reason = "all callers are also sanitize(address = \"off\")"), | |
| sanitize(address = "off") | |
| )] | |
| #[inline(always)] | |
| unsafe fn unlink_free(free_area: *mut Free, bucket: i32) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held, `0 <= bucket < NUMBER_OF_BUCKETS as i32` | |
| // (`bucket_idx < NUMBER_OF_BUCKETS`), and `free_area` is linked in `bucket`. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| let free_size = (*free_area).header.size as usize; | |
| if (*theheap).remaining < free_size { | |
| panic_remaining_underflow((*theheap).remaining, (*free_area).header.size); | |
| } | |
| (*theheap).remaining -= free_size; | |
| let next = (*free_area).next; | |
| let prev = (*free_area).prev; | |
| let bucket_idx = bucket as usize; | |
| assume!(bucket_idx < NUMBER_OF_BUCKETS); | |
| if (*theheap).free_lists[bucket_idx] == free_area { | |
| (*theheap).free_lists[bucket_idx] = next; | |
| if next.is_null() { | |
| clear_free_list_bit(bucket_idx); | |
| } | |
| } | |
| if !prev.is_null() { | |
| (*prev).next = next; | |
| } | |
| if !next.is_null() { | |
| (*next).prev = prev; | |
| } | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `free_area` is a valid node currently | |
| /// linked in its size-corresponding free list bucket. | |
| #[cfg_attr( | |
| not(feature = "userspace"), | |
| expect(inline_no_sanitize, reason = "all callers are also sanitize(address = \"off\")"), | |
| sanitize(address = "off") | |
| )] | |
| #[inline(always)] | |
| unsafe fn unlink_free_unknown_bucket(free_area: *mut Free) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `free_area` is a valid free node whose | |
| // `header.size >= size_of::<Free>()` and maps to its free list bucket `< NUMBER_OF_BUCKETS`. | |
| unsafe { | |
| let size = (*free_area).header.size as usize; | |
| assume!(size >= size_of::<Free>()); | |
| unlink_free(free_area, size_to_index_freeing(size - size_of::<Header>())); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held, `address.cast::<u8>().add(offset)` is within | |
| /// the same OS allocation, 8-byte aligned (`align_of::<Header>()`), and writable for | |
| /// `size_of::<Header>()` bytes, and `size <= u32::MAX as usize`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn create_allocation_header( | |
| address: *mut c_void, | |
| offset: usize, | |
| size: usize, | |
| left: *mut c_void, | |
| ) -> *mut c_void { | |
| // SAFETY: Caller guarantees `address + offset` is an 8-byte aligned writable `Header` location | |
| // within the OS allocation. | |
| unsafe { | |
| let standalone = address.cast::<u8>().add(offset).cast::<Header>(); | |
| (*standalone).left = untag(left); | |
| (*standalone).size = size as u32; | |
| (*standalone).cookie = 0; | |
| standalone.wrapping_add(1).cast::<c_void>() | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `right` points to a valid `Header`. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn fix_left_pointer(right: *mut Header, new_left: *mut Header) { | |
| // SAFETY: Caller guarantees `right` is a valid `Header` pointer. | |
| unsafe { | |
| let tag = (*right).left.addr() & 1; | |
| (*right).left = new_left.map_addr(|addr| (addr & !1) | tag); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `new_area` points to a page-aligned | |
| /// writable OS allocation of `size` bytes (`2 * size_of::<Header>() + size_of::<Free>() <= size <= | |
| /// HEAP_LARGE_ALLOC_BYTES + HEAP_GROW_OVERHEAD`). | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| unsafe fn add_to_heap(new_area: *mut c_void, size: usize) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `new_area` is a page-aligned OS | |
| // allocation of `size` bytes with room for left/right sentinels and a `Free` area. | |
| unsafe { | |
| let top = new_area.cast::<u8>().add(size); | |
| // Set up the left sentinel. Its |left| field will not have FREE_BIT set, | |
| // stopping attempts to coalesce left. | |
| let left_sentinel = new_area.cast::<Header>(); | |
| create_allocation_header(left_sentinel.cast(), 0, size_of::<Header>(), ptr::null_mut()); | |
| // Set up the usable memory area, which will be marked free. | |
| let new_header = left_sentinel.add(1); | |
| let free_size = size - 2 * size_of::<Header>(); | |
| create_free_area(new_header.cast(), left_sentinel.cast(), free_size); | |
| // Set up the right sentinel. Its |left| field will not have FREE_BIT bit | |
| // set, stopping attempts to coalesce right. | |
| let right_sentinel = top.sub(size_of::<Header>()).cast::<Header>(); | |
| create_allocation_header(right_sentinel.cast(), 0, 0, new_header.cast()); | |
| } | |
| } | |
| /// Create a new free-list entry of at least size bytes (including the | |
| /// allocation header). Called with the lock, apart from during init. | |
| /// | |
| /// Returns true on success, false on failure. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| unsafe fn heap_grow(mut size: usize) -> bool { | |
| // This function accesses field members of Header which are poisoned so it | |
| // has to be NO_ASAN. | |
| // We expect to never have been asked to grow by more than the maximum | |
| // allocation | |
| debug_assert!(size <= HEAP_LARGE_ALLOC_BYTES); | |
| // Ensure that after performing the size manipulations below we do not end up | |
| // overflowing the maximum bucket. This check is useful since an obvious | |
| // setting of HEAP_LARGE_ALLOC_BYTES == 1<<HEAP_ALLOC_VIRTUAL_BITS will | |
| // actually result in us growing the heap by *more* than 1<<HEAP_ALLOC_VIRTUAL_BITS | |
| // However this is typically safe since growing by an extra partial page should | |
| // not send us into the next bucket. However if pages are large enough and | |
| // HEAP_ALLOC_VIRTUAL_BITS small enough this could happen, and so this assert | |
| // exists to prevent choosing such sizes. | |
| zr::static_assert!( | |
| (size_to_index_freeing( | |
| page::round_up(HEAP_LARGE_ALLOC_BYTES + HEAP_GROW_OVERHEAD) | |
| - HEAP_GROW_OVERHEAD | |
| - size_of::<Header>(), | |
| ) as usize) | |
| <= NUMBER_OF_BUCKETS | |
| ); | |
| // The new free list entry will have a header on each side (the | |
| // sentinels) so we need to grow the gross heap size by this much more. | |
| size += HEAP_GROW_OVERHEAD; | |
| size = page::round_up(size); | |
| let mut ptr: *mut c_void = ptr::null_mut(); | |
| // SAFETY: Caller guarantees `TheHeapLock` is held. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| let os_alloc = (*theheap).cached_os_alloc; | |
| if !os_alloc.is_null() { | |
| let os_alloc_size = (*os_alloc).size as usize; | |
| if os_alloc_size >= size { | |
| ltracef!( | |
| "Using saved {:#x}-byte OS alloc @{:p} (>={:#x} bytes)\n", | |
| (*os_alloc).size, | |
| os_alloc, | |
| size | |
| ); | |
| ptr = os_alloc.cast(); | |
| size = os_alloc_size; | |
| if cfg!(debug_assertions) | |
| && (!page::is_page_rounded(ptr.addr()) || !page::is_page_rounded(size)) | |
| { | |
| panic_unrounded_os_alloc(size, ptr); | |
| } | |
| } else { | |
| // We need to allocate more from the OS. Return the cached OS | |
| // allocation, in case we're holding an unusually-small block | |
| // that's unlikely to satisfy future calls to heap_grow(). | |
| ltracef!( | |
| "Returning too-small saved {:#x}-byte OS alloc @{:p} (<{:#x} bytes)\n", | |
| (*os_alloc).size, | |
| os_alloc, | |
| size | |
| ); | |
| free_to_os(os_alloc.cast(), os_alloc_size); | |
| } | |
| (*theheap).cached_os_alloc = ptr::null_mut(); | |
| } | |
| if ptr.is_null() { | |
| ptr = heap_page_alloc(size >> page::SHIFT); | |
| if ptr.is_null() { | |
| record_heap_grow_fail(); | |
| return false; | |
| } | |
| ltracef!("Growing heap by {:#x} bytes, new ptr {:p}\n", size, ptr); | |
| (*theheap).size += size; | |
| } | |
| add_to_heap(ptr, size); | |
| } | |
| true | |
| } | |
| /**************************************************** | |
| * | |
| * Public API | |
| * | |
| ****************************************************/ | |
| /// Factors in the header for an allocation. Value chosen here is hard coded and could be less than | |
| /// the actual largest allocation that cmpct_alloc could provide. This is done so that larger | |
| /// buckets can exist in order to allow the heap to grow by amounts larger than what we would like | |
| /// to allow clients to allocate. | |
| pub const HEAP_MAX_ALLOC_SIZE: usize = (1usize << 20) - size_of::<Header>(); | |
| // Ensure that the maximum allocation is actually satisfiable. Note that since | |
| // HEAP_LARGE_ALLOC_BYTES is an internal allocation limit we have to add the header on. We have | |
| // already checked previously that the HEAP_LARGE_ALLOC_BYTES is a valid amount to grow the heap by. | |
| zr::static_assert!( | |
| SizeToIndexRet::size_to_index_allocating(HEAP_MAX_ALLOC_SIZE).rounded_up + size_of::<Header>() | |
| <= HEAP_LARGE_ALLOC_BYTES | |
| ); | |
| /// Allocates `size` bytes from the compact heap, returning null if `size == 0`, | |
| /// `size > HEAP_MAX_ALLOC_SIZE`, or the heap cannot grow. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is not held on the current thread and the heap has | |
| /// been initialized via [`cmpct_init`]. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| pub unsafe fn cmpct_alloc(size: usize) -> *mut c_void { | |
| local_trace_duration!("cmpct_alloc", _trace, "size" => size as u64); | |
| if size == 0 { | |
| return ptr::null_mut(); | |
| } | |
| record_malloc_size(size); | |
| // Large allocations are no longer allowed. See https://fxbug.dev/42106180 for details. | |
| if size > HEAP_MAX_ALLOC_SIZE { | |
| return ptr::null_mut(); | |
| } | |
| let alloc_size = size; | |
| let size = asan::adjust_alloc_size(size, alloc_size); | |
| let SizeToIndexRet { bucket, mut rounded_up } = SizeToIndexRet::size_to_index_allocating(size); | |
| let start_bucket = bucket as usize; | |
| rounded_up += size_of::<Header>(); | |
| lock!(let mut guard = TheHeapLock::get().lock()); | |
| local_trace_duration!("locked", _trace_lock); | |
| let theheap = THE_HEAP.get(); | |
| // SAFETY: `TheHeapLock` is held and `start_bucket < NUMBER_OF_BUCKETS`. | |
| let mut bucket = unsafe { find_nonempty_bucket(start_bucket) }; | |
| if bucket == -1 { | |
| // The maximum amount the heap should grow by is the maximum of: | |
| // * 12% of its total size, | |
| // * The HEAP_USABLE_GROW_SIZE, which is the HEAP_GROW_SIZE minus growth overhead. | |
| // * The size of the desired allocation rounded up to the nearest bucket. | |
| // SAFETY: `TheHeapLock` is held, granting exclusive access to `THE_HEAP`. | |
| let heap_size = unsafe { (*theheap).size }; | |
| let max_growth_threshold = max(max(heap_size >> 3, HEAP_USABLE_GROW_SIZE), rounded_up); | |
| // The actual size we choose to grow by is the minimum of the max_growth_threshold and | |
| // the maximum allocation size cmpctmalloc can handle, which is stored in | |
| // HEAP_LARGE_ALLOC_BYTES. | |
| let mut growby = min(HEAP_LARGE_ALLOC_BYTES, max_growth_threshold); | |
| // Validate that our growby calculation is correct, and that if we grew the heap by | |
| // this amount we would actually satisfy our allocation. | |
| debug_assert!(growby >= rounded_up); | |
| // Try to add a new OS allocation to the heap, reducing the size until | |
| // we succeed or get too small. | |
| ktrace_duration_begin!( | |
| "kernel:memory", | |
| "heap_grow_with_retries", | |
| "allocation_size" => size as u64, | |
| "starting_search_size" => growby as u64 | |
| ); | |
| let mut attempts: i32 = 1; | |
| // SAFETY: `TheHeapLock` is held during each `heap_grow` call. | |
| while !unsafe { heap_grow(growby) } { | |
| if growby <= rounded_up { | |
| guard.as_mut().release(); | |
| // SAFETY: `TheHeapLock` was just released above. | |
| unsafe { heap_report_alloc_failure(rounded_up) }; | |
| ktrace_duration_end!( | |
| "kernel:memory", | |
| "heap_grow_with_retries", | |
| "num_attempts" => attempts, | |
| "status" => "failed" | |
| ); | |
| return ptr::null_mut(); | |
| } | |
| attempts += 1; | |
| growby = max(growby >> 1, rounded_up); | |
| } | |
| ktrace_duration_end!( | |
| "kernel:memory", | |
| "heap_grow_with_retries", | |
| "num_attempts" => attempts, | |
| "status" => "success" | |
| ); | |
| // SAFETY: `TheHeapLock` is held and `start_bucket < NUMBER_OF_BUCKETS`. | |
| bucket = unsafe { find_nonempty_bucket(start_bucket) }; | |
| // It should be the case that, since we hold the heap lock, after growing the heap | |
| // there should be something in our target bucket. However, if there was any confusion | |
| // in calculating the |growby| amount, then it's possible we still do not have | |
| // something. As this could only happen due to a systemic configuration error, and this | |
| // should get caught in tests, this only needs to be a DEBUG_ASSERT and not a always | |
| // enabled ASSERT. Further, it should not be possible for the assertion of the growby | |
| // amount above to succeed and then this assertion to fail. | |
| debug_assert!(bucket != -1); | |
| } | |
| let bucket_idx = bucket as usize; | |
| // SAFETY: `TheHeapLock` is held. Either `find_nonempty_bucket(start_bucket)` found an existing | |
| // non-empty bucket in `start_bucket..NUMBER_OF_BUCKETS`, or `heap_grow(growby)` succeeded with | |
| // `growby >= rounded_up` and populated a bucket `>= start_bucket`, so `0 <= bucket < | |
| // NUMBER_OF_BUCKETS`, `(*theheap).free_lists[bucket_idx]` is a valid non-null `Free` node with | |
| // `header.size >= rounded_up`, and `head`, `result`, and the trailing redzone are backed by | |
| // KASAN shadow memory. | |
| let result = unsafe { | |
| assume!(bucket_idx < NUMBER_OF_BUCKETS); | |
| let head = (*theheap).free_lists[bucket_idx]; | |
| let left_over = ((*head).header.size as usize) - rounded_up; | |
| // We can't carve off the rest for a new free space if it's smaller than the | |
| // free-list linked structure. We also don't carve it off if it's less than | |
| // 1.6% the size of the allocation. This is to avoid small long-lived | |
| // allocations being placed right next to large allocations, hindering | |
| // coalescing and returning pages to the OS. | |
| if left_over >= size_of::<Free>() && left_over > (size >> 6) { | |
| let right = right_header(&raw mut (*head).header); | |
| unlink_free(head, bucket); | |
| let free = head.cast::<u8>().add(rounded_up).cast::<c_void>(); | |
| create_free_area(free, head.cast(), left_over); | |
| fix_left_pointer(right, free.cast::<Header>()); | |
| (*head).header.size -= left_over as u32; | |
| } else { | |
| unlink_free(head, bucket); | |
| } | |
| let result = create_allocation_header( | |
| head.cast(), | |
| 0, | |
| (*head).header.size as usize, | |
| (*head).header.left.cast(), | |
| ); | |
| fill_alloc_area(result, size, rounded_up); | |
| asan::poison_allocation(head.addr(), result.addr(), alloc_size); | |
| result | |
| }; | |
| guard.as_mut().release(); | |
| if alloc_size < FILL_ON_ALLOC_THRESHOLD.load(Ordering::Relaxed) { | |
| // SAFETY: `result` points to a newly allocated, unaliased writable payload of at least | |
| // `alloc_size` bytes, and `Ordering::Relaxed` is sufficient for reading the independent | |
| // `FILL_ON_ALLOC_THRESHOLD` configuration value. | |
| unsafe { ptr::write_bytes(result.cast::<u8>(), 0, alloc_size) }; | |
| } | |
| result | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is held and `header` (`payload - 1`) is a valid | |
| /// allocated `Header` that has not yet been freed. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| unsafe fn cmpct_free_internal(payload: *mut c_void, mut header: *mut Header) { | |
| // SAFETY: Caller guarantees `TheHeapLock` is held and `header` is a valid allocated `Header`. | |
| let header_size = unsafe { | |
| debug_assert!(!is_tagged_as_free(header)); // Double free! | |
| (*header).size | |
| }; | |
| if (header_size as usize) <= size_of::<Header>() { | |
| panic_header_size_too_small(header_size); | |
| } | |
| // SAFETY: `TheHeapLock` is held and `header` is a valid allocated `Header`. | |
| header = | |
| unsafe { asan::quarantine_or_free(THE_HEAP.get(), payload, header, header_size as usize) }; | |
| if header.is_null() { | |
| return; | |
| } | |
| // SAFETY: `TheHeapLock` is held and `header` is a valid allocated (or quarantine-evicted) | |
| // `Header` within an OS allocation bounded by non-free left and right sentinels. | |
| unsafe { | |
| let size = (*header).size as usize; | |
| let left = (*header).left; | |
| if !left.is_null() && is_tagged_as_free(left) { | |
| // Coalesce with left free object. | |
| unlink_free_unknown_bucket(left.cast::<Free>()); | |
| let right = right_header(header); | |
| if is_tagged_as_free(right) { | |
| // Coalesce both sides. | |
| unlink_free_unknown_bucket(right.cast::<Free>()); | |
| let right_right = right_header(right); | |
| fix_left_pointer(right_right, left); | |
| free_memory( | |
| left.cast(), | |
| (*left).left.cast(), | |
| ((*left).size as usize) + size + ((*right).size as usize), | |
| ); | |
| } else { | |
| // Coalesce only left. | |
| fix_left_pointer(right, left); | |
| free_memory(left.cast(), (*left).left.cast(), ((*left).size as usize) + size); | |
| } | |
| } else { | |
| let right = right_header(header); | |
| if is_tagged_as_free(right) { | |
| // Coalesce only right. | |
| let right_right = right_header(right); | |
| unlink_free_unknown_bucket(right.cast::<Free>()); | |
| fix_left_pointer(right_right, header); | |
| free_memory(header.cast(), left.cast(), size + ((*right).size as usize)); | |
| } else { | |
| free_memory(header.cast(), left.cast(), size); | |
| } | |
| } | |
| } | |
| } | |
| /// Associates a user-defined `cookie` with an allocation `ptr` returned from [`cmpct_alloc`] or | |
| /// [`cmpct_memalign`]. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `ptr` is non-null and points to a live allocation returned by | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`], with no concurrent access to its cookie. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| pub unsafe fn cmpct_set_cookie(ptr: *mut c_void, cookie: u32) { | |
| // SAFETY: Caller guarantees `ptr` is a valid non-null allocation payload pointer preceded by | |
| // its `Header`. | |
| unsafe { | |
| let header = ptr.cast::<Header>().sub(1); | |
| (*header).cookie = cookie; | |
| } | |
| } | |
| /// Retrieves the user-defined cookie associated with an allocation `ptr` returned from | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`]. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `ptr` is non-null and points to a live allocation returned by | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`], with no concurrent mutation of its cookie. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| pub unsafe fn cmpct_get_cookie(ptr: *mut c_void) -> u32 { | |
| // SAFETY: Caller guarantees `ptr` is a valid non-null allocation payload pointer preceded by | |
| // its `Header`. | |
| unsafe { | |
| let header = ptr.cast::<Header>().sub(1); | |
| (*header).cookie | |
| } | |
| } | |
| /// Returns the allocation size (including header and rounding) for `ptr`. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `ptr` is non-null and points to a live allocation returned by | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`]. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| pub unsafe fn cmpct_get_size(ptr: *mut c_void) -> u32 { | |
| // SAFETY: Caller guarantees `ptr` is a valid non-null allocation payload pointer preceded by | |
| // its `Header`. | |
| unsafe { | |
| let header = ptr.cast::<Header>().sub(1); | |
| (*header).size | |
| } | |
| } | |
| /// Frees `payload` previously returned by [`cmpct_alloc`] or [`cmpct_memalign`]. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `payload` is null or a live allocation pointer returned by | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`], and `TheHeapLock` is not held on the current thread. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| pub unsafe fn cmpct_free(payload: *mut c_void) { | |
| local_trace_duration!("cmpct_free", _trace); | |
| if payload.is_null() { | |
| return; | |
| } | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| local_trace_duration!("locked", _trace_locked); | |
| // SAFETY: Caller guarantees `payload` is a valid non-null allocation pointer and `TheHeapLock` | |
| // is held. | |
| unsafe { | |
| let header = payload.cast::<Header>().sub(1); | |
| cmpct_free_internal(payload, header); | |
| } | |
| } | |
| /// Frees `payload` of expected size `s` previously returned by [`cmpct_alloc`] or | |
| /// [`cmpct_memalign`]. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `payload` is null or a live allocation pointer returned by | |
| /// [`cmpct_alloc`] or [`cmpct_memalign`] of at least `s` bytes, and `TheHeapLock` is not held on | |
| /// the current thread. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| #[inline] | |
| pub unsafe fn cmpct_sized_free(payload: *mut c_void, s: usize) { | |
| local_trace_duration!("cmpct_free", _trace); | |
| if payload.is_null() { | |
| return; | |
| } | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| local_trace_duration!("locked", _trace_locked); | |
| // SAFETY: Caller guarantees `payload` is a valid non-null allocation pointer and `TheHeapLock` | |
| // is held. | |
| unsafe { | |
| let header = payload.cast::<Header>().sub(1); | |
| // header->size is the size of the heap block |payload| is in, plus size_of::<Header>(), | |
| // plus the difference between the block size and the requested allocation size. If kernel | |
| // ASAN is enabled, it also includes an ASAN redzone. | |
| let header_size = (*header).size; | |
| if (header_size as usize) < s { | |
| panic_sized_free_too_small(header_size, s); | |
| } | |
| cmpct_free_internal(payload, header); | |
| } | |
| } | |
| /// Allocates `size` bytes aligned to `alignment` bytes from the compact heap. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `alignment` is a power of two (or `< 8`), `TheHeapLock` is not | |
| /// held on the current thread, and the heap has been initialized via [`cmpct_init`]. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| pub unsafe fn cmpct_memalign(alignment: usize, size: usize) -> *mut c_void { | |
| local_trace_duration!( | |
| "cmpct_memalign", | |
| _trace, | |
| "alignment" => alignment as u64, | |
| "size" => size as u64 | |
| ); | |
| if size == 0 { | |
| return ptr::null_mut(); | |
| } | |
| if alignment < 8 { | |
| // SAFETY: Caller guarantees the heap is initialized and `TheHeapLock` is not held. | |
| return unsafe { cmpct_alloc(size) }; | |
| } | |
| let padded_size = size.saturating_add(alignment).saturating_add(size_of::<Free>()); | |
| // SAFETY: Caller guarantees the heap is initialized and `TheHeapLock` is not held. | |
| let unaligned = unsafe { cmpct_alloc(padded_size).cast::<u8>() }; | |
| if unaligned.is_null() { | |
| return ptr::null_mut(); | |
| } | |
| lock!(let mut guard = TheHeapLock::get().lock()); | |
| local_trace_duration!("locked", trace_lock); | |
| // SAFETY: `unaligned` points to a live heap allocation of at least `padded_size` bytes backed | |
| // by KASAN shadow memory. | |
| unsafe { | |
| asan::poison_memalign_padding(unaligned.addr(), padded_size); | |
| } | |
| let mask = alignment - 1; | |
| let payload_int = unaligned.addr() + size_of::<Free>() + mask; | |
| let payload = unaligned.with_addr(payload_int & !mask); | |
| if unaligned != payload { | |
| // SAFETY: `TheHeapLock` is held, `unaligned` is a valid allocation of at least | |
| // `size + alignment + size_of::<Free>()` bytes, and `payload.addr() - unaligned.addr() >= | |
| // size_of::<Free>()`, so `unaligned_header` and `header` are valid non-overlapping | |
| // `Header` locations within the same OS allocation. | |
| unsafe { | |
| let unaligned_header = unaligned.cast::<Header>().sub(1); | |
| let header = payload.cast::<Header>().sub(1); | |
| let left_over = payload.addr() - unaligned.addr(); | |
| create_allocation_header( | |
| header.cast(), | |
| 0, | |
| ((*unaligned_header).size as usize) - left_over, | |
| unaligned_header.cast(), | |
| ); | |
| let right = right_header(unaligned_header); | |
| (*unaligned_header).size = left_over as u32; | |
| (*unaligned_header).cookie = 0; | |
| fix_left_pointer(right, header); | |
| } | |
| local_trace_duration_end!(trace_lock); | |
| guard.as_mut().release(); | |
| // SAFETY: `TheHeapLock` was just released above, and `unaligned` now has a valid | |
| // standalone allocation header of size `left_over >= size_of::<Free>()`. | |
| unsafe { cmpct_free(unaligned.cast()) }; | |
| } | |
| // TODO: Free the part after the aligned allocation. | |
| // SAFETY: `payload` points to `size` bytes within the allocation backed by KASAN shadow | |
| // memory. | |
| unsafe { | |
| asan::unpoison_memalign_payload(payload.addr(), size); | |
| } | |
| payload.cast::<c_void>() | |
| } | |
| /// Zero-fill allocations smaller than `size`. | |
| #[inline] | |
| pub fn cmpct_set_fill_on_alloc_threshold(size: usize) { | |
| FILL_ON_ALLOC_THRESHOLD.store(size, Ordering::Relaxed); | |
| } | |
| /// Initializes the compact heap, growing it by [`HEAP_USABLE_GROW_SIZE`]. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is not held and no concurrent heap operations are in | |
| /// flight. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| pub unsafe fn cmpct_init() { | |
| ltrace_entry!(); | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| // SAFETY: `TheHeapLock` is held, granting exclusive access to `THE_HEAP`. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| // Initialize the free lists. | |
| for i in 0..NUMBER_OF_BUCKETS { | |
| (*theheap).free_lists[i] = ptr::null_mut(); | |
| } | |
| for i in 0..BUCKET_WORDS { | |
| (*theheap).free_list_bits[i] = 0; | |
| } | |
| (*theheap).size = 0; | |
| (*theheap).remaining = 0; | |
| (*theheap).cached_os_alloc = ptr::null_mut(); | |
| } | |
| // SAFETY: `TheHeapLock` is held and `THE_HEAP` metadata is initialized. | |
| let success = unsafe { heap_grow(HEAP_USABLE_GROW_SIZE) }; | |
| assert!(success); | |
| debug_fill::on_init(); | |
| } | |
| /// Dumps heap state to the debug log. | |
| /// | |
| /// # Safety | |
| /// | |
| /// If `options` includes [`CmpctDumpOptions::PANIC_TIME`], the caller must ensure that all other | |
| /// CPUs are halted in a kernel panic with no concurrent mutation of `THE_HEAP`. Otherwise, the | |
| /// caller must ensure that `TheHeapLock` is not held on the current thread. | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| pub unsafe fn cmpct_dump(options: CmpctDumpOptions) { | |
| if options.contains(CmpctDumpOptions::PANIC_TIME) { | |
| // If we are panic'ing, just skip the lock. All bets are off anyway. | |
| // SAFETY: Caller guarantees that when `PANIC_TIME` is set, all other CPUs are halted in a | |
| // kernel panic with no concurrent mutation of `THE_HEAP`. | |
| unsafe { cmpct_dump_locked(options) }; | |
| } else { | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| // SAFETY: `TheHeapLock` is held. | |
| unsafe { cmpct_dump_locked(options) }; | |
| } | |
| } | |
| /// Heap byte totals returned by [`cmpct_get_info`]. | |
| #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] | |
| pub struct CmpctInfo { | |
| /// Total bytes allocated from the OS for the heap. | |
| pub used_bytes: usize, | |
| /// Bytes of usable free space in the heap. | |
| pub free_bytes: usize, | |
| /// Bytes held in `cached_os_alloc`. | |
| pub cached_bytes: usize, | |
| } | |
| /// Queries heap byte totals (`used_bytes`, `free_bytes`, `cached_bytes`). | |
| #[cfg_attr(not(feature = "userspace"), sanitize(address = "off"))] | |
| pub fn cmpct_get_info() -> CmpctInfo { | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| // SAFETY: `TheHeapLock` is held, granting exclusive access to `THE_HEAP`. | |
| unsafe { | |
| let theheap = THE_HEAP.get(); | |
| let used_bytes = (*theheap).size; | |
| let free_bytes = (*theheap).remaining; | |
| let cached_bytes = if !(*theheap).cached_os_alloc.is_null() { | |
| (*(*theheap).cached_os_alloc).size as usize | |
| } else { | |
| 0 | |
| }; | |
| CmpctInfo { used_bytes, free_bytes, cached_bytes } | |
| } | |
| } | |
| #[allow(unused_imports)] | |
| pub use tests::cmpct_test; | |
| #[cfg(feature = "heap_enable_tests")] | |
| mod tests { | |
| use core::ffi::c_void; | |
| use core::mem::size_of; | |
| use core::ptr; | |
| use ksync::lock; | |
| use super::{ | |
| CmpctDumpOptions, Free, Header, SizeToIndexRet, THE_HEAP, TheHeapLock, cmpct_alloc, | |
| cmpct_dump, cmpct_free, cmpct_memalign, kprintln, round_up, size_to_index_freeing, | |
| }; | |
| /// Use `HEAP_ENABLE_TESTS` to enable internal testing. The tests are not useful | |
| /// when the target system is up. By that time we have done hundreds of allocations | |
| /// already. | |
| #[inline] | |
| fn cmpct_heap_remaining() -> usize { | |
| lock!(let _guard = TheHeapLock::get().lock()); | |
| // SAFETY: `TheHeapLock` is held. | |
| unsafe { (*THE_HEAP.get()).remaining } | |
| } | |
| fn cmpct_test_buckets() { | |
| // Check for the 8-spaced buckets up to 128. | |
| for i in 1..=128usize { | |
| // Round up when allocating. | |
| let SizeToIndexRet { bucket, rounded_up: rounded } = | |
| SizeToIndexRet::size_to_index_allocating(i); | |
| let bucket = bucket as u32; | |
| let expected = ((round_up(i, 8) >> 3) - 1) as u32; | |
| assert_eq!(bucket, expected); | |
| assert_eq!(rounded & 7, 0); | |
| assert!(rounded >= i); | |
| if i >= size_of::<Free>() - size_of::<Header>() { | |
| // Once we get above the size of the free area struct (4 words), we | |
| // won't round up much for these small size. | |
| assert!(rounded - i < 8); | |
| } | |
| // Only rounded sizes are freed. | |
| if (i & 7) == 0 { | |
| // Up to size 128 we have exact buckets for each multiple of 8. | |
| assert_eq!(bucket, size_to_index_freeing(i) as u32); | |
| } | |
| } | |
| let mut bucket_base: i32 = 7; | |
| let mut j: usize = 16; | |
| while j < 1024 { | |
| // Note the "<=", which ensures that we test the powers of 2 twice to | |
| // ensure that both ways of calculating the bucket number match. | |
| for i in (j * 8)..=(j * 16) { | |
| // Round up to j multiple in this range when allocating. | |
| let SizeToIndexRet { bucket, rounded_up: rounded } = | |
| SizeToIndexRet::size_to_index_allocating(i); | |
| let bucket = bucket as u32; | |
| let expected = (bucket_base as u32) + ((round_up(i, j) / j) as u32); | |
| assert_eq!(bucket, expected); | |
| assert_eq!(rounded % j, 0); | |
| assert!(rounded >= i); | |
| assert!(rounded - i < j); | |
| // Only 8-rounded sizes are freed or chopped off the end of a free | |
| // area when allocating. | |
| if (i & 7) == 0 { | |
| // When freeing, if we don't hit the size of the bucket | |
| // precisely, we have to put the free space into a smaller | |
| // bucket, because the buckets have entries that will always | |
| // be big enough for the corresponding allocation size (so we | |
| // don't have to traverse the free chains to find a big enough | |
| // one). | |
| if (i % j) == 0 { | |
| assert_eq!(bucket as i32, size_to_index_freeing(i)); | |
| } else { | |
| assert_eq!((bucket as i32) - 1, size_to_index_freeing(i)); | |
| } | |
| } | |
| } | |
| j *= 2; | |
| bucket_base += 8; | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is not held and the heap is initialized. | |
| unsafe fn cmpct_test_get_back_newly_freed_helper(size: usize) { | |
| // SAFETY: Test helper exercises `cmpct_alloc` and `cmpct_free`. | |
| unsafe { | |
| let allocated = cmpct_alloc(size); | |
| if allocated.is_null() { | |
| return; | |
| } | |
| let allocated2 = cmpct_alloc(8).cast::<u8>(); | |
| let expected_position = allocated.cast::<u8>().wrapping_add(size); | |
| if allocated2 < expected_position || allocated2 > expected_position.wrapping_add(128) { | |
| // If the allocated2 allocation is not in the same OS allocation as the | |
| // first allocation then the test may not work as expected (the memory | |
| // may be returned to the OS when we free the first allocation, and we | |
| // might not get it back). | |
| cmpct_free(allocated); | |
| cmpct_free(allocated2.cast()); | |
| return; | |
| } | |
| cmpct_free(allocated); | |
| let allocated3 = cmpct_alloc(size); | |
| // To avoid churn and fragmentation we would want to get the newly freed | |
| // memory back again when we allocate the same size shortly after. | |
| assert_eq!(allocated3, allocated); | |
| cmpct_free(allocated2.cast()); | |
| cmpct_free(allocated3); | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is not held and the heap is initialized. | |
| unsafe fn cmpct_test_get_back_newly_freed() { | |
| // SAFETY: Calls `cmpct_test_get_back_newly_freed_helper`. | |
| unsafe { | |
| let mut increment = 16usize; | |
| let mut i = 128usize; | |
| while i <= 0x8000000 { | |
| let mut j = i; | |
| while j < i * 2 { | |
| cmpct_test_get_back_newly_freed_helper(i - 8); | |
| cmpct_test_get_back_newly_freed_helper(i); | |
| cmpct_test_get_back_newly_freed_helper(i + 1); | |
| j += increment; | |
| } | |
| i *= 2; | |
| increment *= 2; | |
| } | |
| for k in 1024..=2048 { | |
| cmpct_test_get_back_newly_freed_helper(k); | |
| } | |
| } | |
| } | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that `TheHeapLock` is not held and the heap is initialized. | |
| unsafe fn cmpct_test_return_to_os() { | |
| // SAFETY: Test helper exercises `cmpct_alloc` and `cmpct_free`. | |
| unsafe { | |
| let remaining = cmpct_heap_remaining(); | |
| // This goes in a new OS allocation since the trim above removed any free | |
| // area big enough to contain it. | |
| let a = cmpct_alloc(5000); | |
| let b = cmpct_alloc(2500); | |
| cmpct_free(a); | |
| cmpct_free(b); | |
| // If things work as expected the new allocation is at the start of an OS | |
| // allocation. There's just one sentinel and one header to the left of it. | |
| // It that's not the case then the allocation was met from some space in | |
| // the middle of an OS allocation, and our test won't work as expected, so | |
| // bail out. | |
| if (a.addr() & (page::SIZE - 1)) != size_of::<Header>() * 2 { | |
| return; | |
| } | |
| // No trim needed when the entire OS allocation is free. | |
| assert_eq!(remaining, cmpct_heap_remaining()); | |
| } | |
| } | |
| /// Runs internal `cmpctmalloc` self-tests when `heap_enable_tests` is enabled. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that the heap has been initialized and `TheHeapLock` | |
| /// is not held on the current thread. | |
| pub unsafe fn cmpct_test() { | |
| let mut rng_state: u32 = 1; | |
| let mut rand = || -> u32 { | |
| rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345); | |
| (rng_state >> 16) & 0x7fff | |
| }; | |
| cmpct_test_buckets(); | |
| // SAFETY: Caller guarantees the heap is initialized and `TheHeapLock` is not held. | |
| unsafe { | |
| cmpct_test_get_back_newly_freed(); | |
| cmpct_test_return_to_os(); | |
| cmpct_dump(CmpctDumpOptions::NONE); | |
| } | |
| let mut ptr: [*mut c_void; 16] = [ptr::null_mut(); 16]; | |
| // SAFETY: Exercises `cmpct_alloc`, `cmpct_free`, and `cmpct_dump` with valid sizes and | |
| // pointers while `TheHeapLock` is not held. | |
| unsafe { | |
| ptr[0] = cmpct_alloc(8); | |
| ptr[1] = cmpct_alloc(32); | |
| ptr[2] = cmpct_alloc(7); | |
| ptr[3] = cmpct_alloc(0); | |
| ptr[4] = cmpct_alloc(98713); | |
| ptr[5] = cmpct_alloc(16); | |
| cmpct_free(ptr[5]); | |
| cmpct_free(ptr[1]); | |
| cmpct_free(ptr[3]); | |
| cmpct_free(ptr[0]); | |
| cmpct_free(ptr[4]); | |
| cmpct_free(ptr[2]); | |
| cmpct_dump(CmpctDumpOptions::NONE); | |
| } | |
| ptr.fill(ptr::null_mut()); | |
| for i in 0..32768 { | |
| let index = (rand() as usize) % 16; | |
| if (i % (16 * 1024)) == 0 { | |
| kprintln!("pass {}", i); | |
| } | |
| // printf("index 0x%x\n", index); | |
| if !ptr[index].is_null() { | |
| // printf("freeing ptr[0x%x] = %p\n", index, ptr[index]); | |
| // SAFETY: `ptr[index]` is a valid non-null allocation returned by `cmpct_memalign`. | |
| unsafe { cmpct_free(ptr[index]) }; | |
| ptr[index] = ptr::null_mut(); | |
| } | |
| let align = 1usize << (rand() % 8); | |
| // SAFETY: `align` is a power of two and the heap is initialized. | |
| ptr[index] = unsafe { cmpct_memalign(align, (rand() as usize) % 32768) }; | |
| // printf("ptr[0x%x] = %p, align 0x%x\n", index, ptr[index], align); | |
| debug_assert_eq!(ptr[index].addr() % align, 0); | |
| // cmpct_dump(CmpctDumpOptions::NONE); | |
| } | |
| for slot in ptr { | |
| if !slot.is_null() { | |
| // SAFETY: `slot` is a valid non-null allocation returned by `cmpct_memalign`. | |
| unsafe { cmpct_free(slot) }; | |
| } | |
| } | |
| // SAFETY: `TheHeapLock` is not held. | |
| unsafe { cmpct_dump(CmpctDumpOptions::NONE) }; | |
| } | |
| } | |
| #[cfg(not(feature = "heap_enable_tests"))] | |
| mod tests { | |
| /// Runs internal `cmpctmalloc` self-tests when `heap_enable_tests` is enabled. | |
| /// | |
| /// # Safety | |
| /// | |
| /// The caller must ensure that the heap has been initialized and `TheHeapLock` | |
| /// is not held on the current thread. | |
| pub unsafe fn cmpct_test() {} | |
| } |