blob: 6cee2a4c531984ea89fdf868ce16d0206f999c7b [file] [edit]
/* ----------------------------------------------------------------------------
Copyright (c) 2019-2026, Microsoft Research, Daan Leijen
This is free software; you can redistribute it and/or modify it under the
terms of the MIT license. A copy of the license can be found in the file
"LICENSE" at the root of this distribution.
-----------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------
Allocate a page (`_mi_arenas_page_alloc`): either reclaim an abandoned page, or allocate the memory
and meta data of a fresh arena or OS page and initialize it. Also frees pages again.
-----------------------------------------------------------------------------*/
#include "mimalloc.h"
#include "mimalloc/prim/prim-tls.h"
#include "mimalloc/util/stats.h"
#include "mimalloc/util/memid.h"
#include "mimalloc/alloc/page.h"
#include "mimalloc/arena/arena-alloc.h"
#include "mimalloc/profile/sample-guarded.h" // _mi_page_unguard_all
#include "mimalloc/arena/arena-page.h"
#include "arena-page.h"
/* -----------------------------------------------------------
Guard pages in front of and right after the blocks of a page.
These are used in secure mode for the pages of meta theaps,
at the end of singleton pages (huge blocks), and at the end of all pages at secure level 5.
The `memid.guard_front/end` flags record the guard pages of a page.
----------------------------------------------------------- */
// The guard pages a theap uses for a fresh page of `size` bytes. To limit the overhead, we only use guard pages
// if together they take at most 1/4 of the page. OS aligned pages have no front guard page.
// There are no guard pages if the theap allocates from an exclusive pinned arena (as pinned memory cannot be protected).
static mi_page_guards_t mi_theap_page_guards(const mi_theap_t* theap, size_t size, bool singleton, bool os_align) {
const size_t gsize = _mi_os_secure_guard_page_size();
const bool guard_end = (theap->guard_end || (singleton && MI_SECURE >= 1)); // in secure mode, huge blocks are always followed by a guard page
mi_page_guards_t guards = { 0, 0 };
const mi_arena_t* const req_arena = mi_theap_heap(theap)->exclusive_arena;
if (req_arena != NULL && req_arena->memid.is_pinned) return guards;
if (theap->guard_front && !os_align && gsize <= size/4) { guards.front = gsize; }
if (guard_end && guards.front + gsize <= size/4) { guards.end = gsize; }
return guards;
}
// The offset of the first block after a front guard page (of at least 4 KiB which is the maximal natural block alignment)
static size_t mi_page_guard_front_block_start(size_t guard_front, size_t block_size) {
return mi_align_up(guard_front, mi_bounds_align(block_size)); // on CHERI, keep exact bounds
}
// Can we offset the first block to spread cache sets? (see `MI_PAGE_CACHE_OFFSET`)
static bool mi_page_block_can_cache_offset(size_t block_size, bool os_align) {
if (os_align || !mi_block_size_has_cache_offset(block_size)) return false;
#if MI_GUARDED
if (block_size % _mi_os_page_size() == 0) return false; // guarded blocks must stay OS page aligned
#endif
return true;
}
/* -----------------------------------------------------------
Allocate the memory area and page meta data for a fresh page
(`mi_arenas_page_alloc_fresh_area`)
----------------------------------------------------------- */
// Offset of the first block when the page meta data is in front of the slices.
static size_t mi_page_meta_in_front_block_start(size_t block_size, bool os_align) {
size_t offset;
#if MI_GUARDED
// in a guarded build, we align pages with blocks a multiple of an OS page size, to the OS page size
// this ensures that all blocks in such pages are OS page size aligned (which is needed for the guard pages)
const size_t os_page_size = _mi_os_page_size();
mi_assert_internal(MI_PAGE_ALIGN >= os_page_size);
if (!os_align && block_size % os_page_size == 0 && block_size > os_page_size /* at least 2 or more */ ) {
offset = mi_align_up(mi_page_info_size(), os_page_size);
}
else
#endif
if (os_align) {
offset = MI_PAGE_ALIGN;
}
else if (block_size != 0 && mi_is_power_of_two(block_size) && block_size <= MI_PAGE_MAX_START_BLOCK_ALIGN2) {
// naturally align power-of-2 blocks up to MI_PAGE_MAX_START_BLOCK_ALIGN2 size (4KiB)
offset = mi_align_up(mi_page_info_size(), block_size);
}
else if (block_size != 0 && block_size <= MI_SMALL_SIZE_MAX) {
// align small blocks to their size
offset = mi_align_up(mi_page_info_size(), block_size);
}
else if (block_size != 0 && (block_size % MI_PAGE_OSPAGE_BLOCK_ALIGN2) == 0) {
// also align large pages that are a multiple of MI_PAGE_OSPAGE_BLOCK_ALIGN2 (4KiB)
offset = mi_align_up(mi_page_info_size(), MI_PAGE_OSPAGE_BLOCK_ALIGN2);
}
else {
// otherwise start after the info
offset = mi_page_info_size();
}
// on CHERI, also align such that every block has exact bounds (os_align is already aligned enough)
return mi_align_up(offset, (os_align ? MI_MAX_ALIGN_SIZE : mi_max(MI_MAX_ALIGN_SIZE, mi_bounds_align(block_size))));
}
static size_t mi_page_cache_offset_index(uint8_t* slice_start, size_t slice_count, size_t noffsets) {
// Fibonacci hash of the page index: the top bits spread consecutive pages evenly over the offsets
const size_t page_index = (size_t)(mi_addr(slice_start) / MI_ARENA_SLICE_SIZE) / slice_count;
#if MI_SIZE_BITS >= 64
const size_t hash = page_index * MI_ZU(0x9E3779B97F4A7C15);
#else
const size_t hash = page_index * MI_ZU(0x9E3779B9);
#endif
return (((hash >> (MI_SIZE_BITS - 16)) * noffsets) >> 16);
}
static size_t mi_page_small_block_cache_offset(uint8_t* slice_start, size_t slice_count, size_t block_size) {
const size_t unit = mi_max(MI_PAGE_SMALL_CACHE_OFFSET_UNIT, mi_natural_alignment_of_size(block_size));
if (unit > MI_PAGE_SMALL_CACHE_OFFSET_MAX) return 0;
return mi_page_cache_offset_index(slice_start, slice_count, 1 + MI_PAGE_SMALL_CACHE_OFFSET_MAX/unit) * unit;
}
// Cache offset for the first block to spread the blocks of different pages over the cache sets (issue #1121).
// The offset fits in the unused space at the end of the page; if there is none, we skip the last block
// if that wastes less than 12.5% of the page (e.g. 64 KiB blocks in a 4 MiB page, but not 512 KiB blocks).
static size_t mi_page_block_cache_offset(uint8_t* slice_start, size_t slice_count, size_t block_size, size_t block_start, bool os_align, size_t page_noguard_size) {
if (MI_PAGE_CACHE_OFFSET == 0 || os_align) return 0;
if (!mi_block_size_has_cache_offset(block_size)) return mi_page_small_block_cache_offset(slice_start, slice_count, block_size);
if (!mi_page_block_can_cache_offset(block_size, os_align)) return 0;
const size_t unit = mi_max(MI_PAGE_CACHE_OFFSET_UNIT, mi_bounds_align(block_size)); // on CHERI, keep exact bounds
if (unit > MI_PAGE_CACHE_OFFSET_MAX) return 0;
mi_assert_internal(block_start + block_size <= page_noguard_size);
size_t slack = (page_noguard_size - block_start) % block_size;
if (slack < unit && block_size < page_noguard_size/8) { slack += block_size; }
const size_t noffsets = 1 + (slack < MI_PAGE_CACHE_OFFSET_MAX ? slack : MI_PAGE_CACHE_OFFSET_MAX) / unit;
return mi_page_cache_offset_index(slice_start, slice_count, noffsets) * unit;
}
// Use the separate (zero initialized) `page_meta` if available and not preferring in front of the slices,
// and otherwise put the page meta data in front of the slices (without a front guard page).
// Also determines the offset of the first block.
static mi_page_t* mi_page_meta_select(uint8_t* slice_start, size_t slice_count, mi_page_t* page_meta, size_t block_size, bool os_align,
mi_page_guards_t* guards, size_t* block_start) {
mi_assert_internal(page_meta == NULL || page_meta->block_size == 0);
mi_page_t* page;
size_t offset;
if (page_meta == NULL
#if MI_PAGE_META_SMALL_IS_ALIGNED
// small pages keep the page meta data in front of the slices for a fast `mi_free_small`
// (note: it is important that the separate `page_meta->block_size == 0` for `mi_arena_page_at_slice`)
|| (!os_align && block_size <= MI_SMALL_MAX_OBJ_SIZE)
#endif
) {
// put the actual page meta info in front of the slices
// (note: it is not initialized yet since the slices may not be committed yet)
offset = mi_page_meta_in_front_block_start(block_size, os_align);
page = (mi_page_t*)slice_start;
guards->front = 0;
}
else {
// use the separate page meta data
mi_assert_internal(guards->front == 0 || guards->front >= MI_PAGE_MAX_START_BLOCK_ALIGN2);
mi_memzero_aligned(page_meta, sizeof(*page_meta));
offset = mi_page_guard_front_block_start(guards->front, block_size);
page = page_meta;
}
offset += mi_page_block_cache_offset(slice_start, slice_count, block_size, offset, os_align, _mi_size_of_slices(slice_count) - guards->end);
mi_assert_internal(os_align || offset < MI_ARENA_SLICE_SIZE); // so `mi_page_slice_start` can align down the page start
*block_start = offset;
return mi_page_meta_bounded(page);
}
#if MI_PAGE_META_IS_ALIGNED
// Ensure the aligned page meta data for the slices at `slice_start` is committed.
static bool mi_arena_page_meta_ensure_committed(mi_arena_t* arena, uint8_t* slice_start) {
uint8_t* const meta_slices = (uint8_t*)mi_align_down_ptr(slice_start,MI_PAGE_META_ALIGNMENT) + MI_PAGE_META_ALIGNED_OFFSET;
mi_assert_internal(meta_slices >= mi_arena_start(arena));
const size_t meta_slice_index = (meta_slices - mi_arena_start(arena)) / MI_ARENA_SLICE_SIZE;
if mi_unlikely(mi_bitmap_is_clear(arena->slices_committed, meta_slice_index)) {
// try to commit all page meta slices now (but not the guard slices around it)
const size_t meta_slice_count = mi_arena_page_meta_aligned_array_slices();
// the following assertion does not hold in a concurrent setting..
// mi_assert_internal(mi_bitmap_is_clearN(arena->slices_committed, meta_slice_index, meta_slice_count));
const size_t commit_size = meta_slice_count * MI_ARENA_SLICE_SIZE;
if (!_mi_arena_commit(arena->subproc, arena, meta_slices, commit_size, NULL, 0)) {
return false;
}
mi_bitmap_setN(arena->slices_committed, meta_slice_index, meta_slice_count, NULL);
}
return true;
}
#endif
// Try to allocate slices from free space in the arena's, and return the separate page meta data (if available) in `*ppage_meta`.
static uint8_t* mi_arenas_page_alloc_from_slices(mi_theap_t* theap, size_t slice_count, bool commit, bool allow_large,
mi_memid_t* memid, mi_arena_pages_t** parena_pages, mi_page_t** ppage_meta) {
mi_heap_t* const heap = mi_theap_heap(theap);
mi_tld_t* const tld = theap->tld;
const int numa_node = (heap->numa_node >= 0 ? heap->numa_node : tld->numa_node);
uint8_t* const start = (uint8_t*)_mi_arenas_try_alloc(heap, slice_count, MI_ARENA_SLICE_ALIGN, commit, allow_large, heap->exclusive_arena, tld->thread_seq, numa_node, memid);
if (start == NULL) return NULL;
mi_arena_t* const arena = mi_memid_arena(*memid);
// ensure the arena pages (and the aligned page meta data)
mi_arena_pages_t* const arena_pages = _mi_heap_ensure_arena_pages(heap, arena);
if (arena_pages==NULL
#if MI_PAGE_META_IS_ALIGNED
|| !mi_arena_page_meta_ensure_committed(arena, start)
#endif
) {
_mi_arenas_free(heap->subproc, start, _mi_size_of_slices(slice_count), *memid); // roll back
return NULL;
}
// note: the following assert should hold if we could check it atomically, but in a concurrent setting we may already allocate in slice_count
// mi_assert_internal(mi_bitmap_is_clearN(arena_pages->pages, memid->mem.arena.slice_index, memid->mem.arena.slice_count));
mi_assert_internal(mi_bitmap_is_clear(arena_pages->pages, memid->mem.arena.slice_index));
// don't set yet: mi_bitmap_set(arena_pages->pages, memid->mem.arena.slice_index);
*parena_pages = arena_pages;
// the separate page meta data (if available)
#if MI_PAGE_META_IS_ALIGNED
*ppage_meta = mi_aligned_ptr_page0(start);
#else
mi_assert_internal(MI_PAGE_META_IS_SEPARATED!=0 || arena->pages_meta == NULL);
*ppage_meta = (arena->pages_meta == NULL ? NULL : mi_page_meta_load(mi_arena_page_meta_at_slice(arena, memid->mem.arena.slice_index)));
#endif
return start;
}
#if MI_PAGE_META_OS_IS_SEPARATED
#if MI_PAGE_META_IS_ALIGNED
// The size of the aligned page meta data up to and including the entries for a page at `page_offset` in an aligned OS area.
static size_t mi_page_meta_os_aligned_size(size_t page_offset, size_t max_page_meta_count) {
size_t count = 0;
const size_t slice_end = mi_divide_up(page_offset + MI_ARENA_SLICE_SIZE, MI_ARENA_SLICE_SIZE) + max_page_meta_count;
for (size_t slice_idx = page_offset / MI_ARENA_SLICE_SIZE; slice_idx < slice_end; slice_idx++) {
count = mi_max(count, mi_page_meta_aligned_index(slice_idx) + 1);
}
return count * sizeof(mi_page_t);
}
#endif
// Allocate slices from the OS with the (committed and zero'd) page meta data in front of the page area (returned in `*ppage_meta`).
// The OS memory is `[guard slice | page meta data | guard slice | ... | page area]` where the guard slices are only reserved in secure mode.
// With aligned page meta data, the OS memory is `MI_PAGE_META_ALIGNMENT` aligned and the page meta data is indexed by slice (see `mi_aligned_ptr_page`).
static uint8_t* mi_arenas_page_alloc_from_os(mi_heap_t* heap, size_t slice_count, size_t max_page_meta_count, size_t block_alignment, bool os_align, bool commit, bool allow_large,
mi_memid_t* memid, mi_page_t** ppage_meta) {
MI_UNUSED(allow_large); MI_UNUSED(max_page_meta_count); MI_UNUSED(os_align);
mi_arena_t* const req_arena = heap->exclusive_arena;
const size_t alloc_size = _mi_size_of_slices(slice_count);
const size_t page_alignment = (block_alignment < MI_PAGE_ALIGN ? MI_PAGE_ALIGN : block_alignment);
#if MI_PAGE_META_IS_ALIGNED
if (block_alignment >= MI_PAGE_META_ALIGNMENT) {
// if we allow alignment >= MI_PAGE_META_ALIGNMENT we need to substract 1 from a pointer
// in mi_aligned_ptr_page (and test for (intptr_t)p < 0 instead of NULL). We avoid this by limiting the max alignment.
_mi_warning_message("requested alignment is too large (%zu KiB)\n", block_alignment / MI_KiB);
errno = EINVAL;
return NULL;
}
const size_t os_alignment = MI_PAGE_META_ALIGNMENT;
#else
const size_t os_alignment = page_alignment;
#endif
// offset of the page area (leaving room for the guard slices around the page meta data)
size_t page_offset = mi_align_up(_mi_size_of_slices(2*MI_ARENA_GUARD_SLICES + 1), page_alignment);
#if MI_PAGE_META_IS_ALIGNED
size_t page_meta_size = mi_page_meta_os_aligned_size(page_offset, max_page_meta_count);
while (_mi_size_of_slices(MI_ARENA_GUARD_SLICES) + mi_align_up(page_meta_size,_mi_os_page_size()) + _mi_os_secure_guard_page_size() > page_offset) {
// the spread page meta entries need more room in front of the page area
page_offset += page_alignment;
page_meta_size = mi_page_meta_os_aligned_size(page_offset, max_page_meta_count);
}
#else
const size_t page_meta_size = sizeof(mi_page_t);
#endif
uint8_t* const os_start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size + page_offset, os_alignment, 0 /* align offset */, false /* commit */, false /* allow large */, req_arena, memid);
if (os_start==NULL) return NULL;
uint8_t* const start = os_start + page_offset;
// commit the page meta data and page area
uint8_t* const page_meta_start = os_start + _mi_size_of_slices(MI_ARENA_GUARD_SLICES);
mi_assert_internal(page_meta_start + mi_align_up(page_meta_size,_mi_os_page_size()) + _mi_os_secure_guard_page_size() <= start);
bool is_zero;
bool ok = _mi_arena_commit(heap->subproc,req_arena,page_meta_start,page_meta_size,&is_zero,page_meta_size /* don't count in stats? */);
if (ok && commit) {
ok = _mi_arena_commit(heap->subproc,req_arena,start,alloc_size,NULL,0);
}
if (!ok) { _mi_os_free(heap->subproc,os_start,alloc_size+page_offset,*memid); return NULL; }
if (!is_zero && !memid->initially_zero) {
mi_memzero_aligned(page_meta_start,page_meta_size);
}
#if MI_ARENA_GUARD_SLICES > 0
// set guard pages around the page meta data (if not initially committed, the guards are never committed)
if (memid->initially_committed && !memid->is_pinned) {
_mi_os_secure_guard_pages_set_around(heap->subproc, page_meta_start, page_meta_size, *memid);
}
#endif
if (commit) {
memid->initially_committed = true; // so we don't commit again
mi_memid_os_set_committed(memid, alloc_size);
}
// and determine the page meta data
mi_assert_internal(mi_is_aligned(start,page_alignment));
#if MI_PAGE_META_IS_ALIGNED
mi_assert_internal(mi_is_aligned(os_start,MI_PAGE_META_ALIGNMENT));
mi_assert_internal(mi_align_down_ptr(start-1,MI_PAGE_META_ALIGNMENT) == os_start);
mi_assert_internal((uint8_t*)mi_aligned_ptr_page0(start) < page_meta_start + page_meta_size);
*ppage_meta = mi_aligned_ptr_page0(start);
#else
*ppage_meta = (mi_page_t*)mi_ptr_bounded_data(page_meta_start, sizeof(mi_page_t));
#endif
return start;
}
#else
// Allocate slices from the OS with the page meta data in front of the slices (so there is no separate page meta data).
static uint8_t* mi_arenas_page_alloc_from_os(mi_heap_t* heap, size_t slice_count, size_t max_page_meta_count, size_t block_alignment, bool os_align, bool commit, bool allow_large,
mi_memid_t* memid, mi_page_t** ppage_meta) {
MI_UNUSED(max_page_meta_count);
const size_t page_alignment = MI_ARENA_SLICE_ALIGN;
const size_t alloc_size = _mi_size_of_slices(slice_count);
uint8_t* start;
if (os_align) {
// note: slice_count already includes the page meta data (of MI_PAGE_ALIGN size) in front of the aligned block
start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size, block_alignment, page_alignment /* align offset */, commit, allow_large, heap->exclusive_arena, memid);
mi_assert_internal(start==NULL || mi_is_aligned(start + page_alignment, block_alignment));
}
else {
start = (uint8_t*)_mi_arena_os_alloc_aligned(heap->subproc, alloc_size, page_alignment, 0 /* align offset */, commit, allow_large, heap->exclusive_arena, memid);
}
if (start == NULL) return NULL;
*ppage_meta = NULL;
return start;
}
#endif
// Allocate the slices for a fresh page and return its separate page meta data (if available) in `*ppage_meta`.
static uint8_t* mi_arenas_page_alloc_fresh_area(mi_theap_t* theap, size_t slice_count, size_t max_page_meta_count, size_t block_alignment, bool os_align, bool commit, bool allow_large,
mi_memid_t* memid, mi_arena_pages_t** parena_pages, mi_page_t** ppage_meta ) {
mi_assert_internal(parena_pages!=NULL && ppage_meta!=NULL);
*parena_pages = NULL;
*ppage_meta = NULL;
*memid = _mi_memid_none();
mi_heap_t* const heap = mi_theap_heap(theap);
// try to allocate from free space in arena's
uint8_t* slice_start = NULL;
if (!mi_option_is_enabled(mi_option_disallow_arena_alloc) && // allowed to allocate from arena's?
!os_align && // not large alignment
slice_count <= mi_arena_max_object_size()/MI_ARENA_SLICE_SIZE) // and not too large
{
slice_start = mi_arenas_page_alloc_from_slices(theap, slice_count, commit, allow_large, memid, parena_pages, ppage_meta);
}
// otherwise fall back to the OS
if (slice_start == NULL) {
slice_start = mi_arenas_page_alloc_from_os(heap, slice_count, max_page_meta_count, block_alignment, os_align, commit, allow_large, memid, ppage_meta);
if (slice_start == NULL) return NULL;
mi_heap_stat_increase(heap,pages_os_allocated,1);
mi_atomic_increment_relaxed(&heap->os_page_count);
}
mi_assert_internal(mi_is_aligned(slice_start, MI_PAGE_ALIGN));
return slice_start;
}
/* -----------------------------------------------------------
Allocate and initialize a fresh page (`mi_arenas_page_alloc_fresh`)
----------------------------------------------------------- */
// Commit the start of the page up to and including the first block if the slices are not yet committed.
// Sets `*commit_size` to the committed size (or 0 if the slices were already committed).
// Never commits beyond `page_end` (the end guard page or the end of the page).
static bool mi_arenas_page_commit_first_block(mi_theap_t* theap, mi_memid_t memid, uint8_t* slice_start, size_t block_start, size_t block_size, size_t page_end, size_t* commit_size) {
*commit_size = 0;
if (memid.initially_committed) return true;
mi_assert_internal(!memid.guard_front); // as the front guard page would never be recommitted (see `_mi_arenas_page_free_prim`)
size_t csize = mi_align_up(block_start + block_size, mi_page_min_commit_size());
if (csize > page_end) { csize = page_end; }
bool is_zero = false;
if mi_unlikely(!_mi_arena_commit(mi_theap_subproc(theap), mi_memid_arena(memid), slice_start, csize, &is_zero, 0)) {
return false;
}
*commit_size = csize;
return true;
}
// Prepare the page memory: zero the page meta data if it is in front of the slices, set the guard pages, etc.
// The end guard page (if any) is at `page_end`.
static void mi_arenas_page_init_memory(mi_theap_t* theap, mi_page_t* page, uint8_t* slice_start, size_t slice_count, size_t block_start, size_t page_end, mi_memid_t* memid)
{
MI_UNUSED(slice_count); MI_UNUSED(block_start);
// zero initialize the page meta data
if (!memid->initially_zero && (uint8_t*)page == slice_start) {
mi_memzero_aligned(page, sizeof(*page));
}
// claimed free slices: initialize the page partly
if (!memid->initially_zero && memid->initially_committed) {
mi_track_mem_undefined(slice_start, slice_count * MI_ARENA_SLICE_SIZE);
}
else if (memid->initially_committed) {
mi_track_mem_defined(slice_start, slice_count * MI_ARENA_SLICE_SIZE);
}
#if MI_DEBUG > 1
if (memid->initially_zero && memid->initially_committed) {
if (!mi_mem_is_zero(slice_start, page_end)) {
_mi_error_message(EFAULT, "internal error: page memory was not zero initialized.\n");
memid->initially_zero = false;
if (block_start > 0) { mi_memzero_aligned(page, sizeof(*page)); }
}
}
#endif
// set the guard pages
mi_subproc_t* const subproc = mi_theap_subproc(theap);
if (memid->guard_front) { _mi_os_secure_guard_page_set_at(subproc, slice_start, *memid); }
if (memid->guard_end) { _mi_os_secure_guard_page_set_at(subproc, slice_start + page_end, *memid); }
}
// With memory tagging, give the page meta data a fresh random tag. Page meta data in front of the slices
// has the opposite tag class of the first block, the separate arena page meta entries have a tag that differs
// from the adjacent entries (see `arena.h:mi_arena_page_meta_memtag`), and the separate page meta data of pages allocated
// directly from the OS (which is not adjacent to other page meta data) gets a tag from the full range.
// Free arena memory is untagged, but the separate arena page meta entries always have a random tag (see `arena.c:mi_arena_memtag_init`).
// (note: aligned page meta data is never used with memory tagging as that always uses `MI_FREE_IS_CHECKED`)
static mi_page_t* mi_page_meta_memtag(mi_page_t* page, uint8_t* slice_start, mi_memid_t memid) {
if (!mi_memtag_use_for_meta(memid.is_memtag)) return page;
if (mi_addr(page) == mi_addr(slice_start)) {
return mi_page_meta_bounded(mi_memtag_meta_init_class(page, sizeof(mi_page_t), 1));
}
mi_arena_t* const arena = mi_memid_arena(memid);
if (arena != NULL && arena->pages_meta != NULL) {
mi_assert_internal(mi_addr(page) == mi_addr(mi_arena_page_meta_at_slice(arena, memid.mem.arena.slice_index)));
return mi_page_meta_bounded(mi_arena_page_meta_memtag(arena, page));
}
#if MI_PAGE_META_IS_ALIGNED
return mi_page_meta_bounded(mi_memtag_meta_init_class(page, sizeof(mi_page_t), mi_page_meta_memtag_idx(page))); // alternate the tag class in aligned arrays
#else
return mi_page_meta_bounded(mi_memtag_meta_init_separate(page, sizeof(mi_page_t), mi_page_meta_memtag_idx(page), NULL));
#endif
}
// Is this page meta data an entry of the separate arena page meta data (`arena->pages_meta`)?
static bool mi_page_meta_is_arena_entry(mi_page_t* page) {
#if MI_PAGE_META_IS_ALIGNED
MI_UNUSED(page); return false;
#else
return (page->memid.memkind == MI_MEM_ARENA && mi_page_meta_is_separated(page));
#endif
}
// After freeing a page its meta data cannot be accessed anymore: separate arena page meta entries get a fresh
// random tag while other page meta data is untagged again (as free arena memory).
static void mi_page_meta_memtag_done(mi_page_t* page, mi_memid_t memid) {
if (!mi_memtag_use_for_meta(memid.is_memtag)) return;
if (mi_page_meta_is_arena_entry(page)) { mi_arena_page_meta_memtag(mi_memid_arena(memid), page); }
else { mi_memtag_clear(page, sizeof(mi_page_t)); }
}
// Initialize the fields of the page meta data.
static void mi_arenas_page_init_meta(mi_theap_t* theap, mi_page_t* page, uint8_t* slice_start, size_t block_start, size_t block_size,
size_t reserved, size_t commit_size, mi_memid_t memid)
{
mi_assert_internal(reserved > 0 && reserved <= UINT16_MAX);
// initialize the page start
uint8_t* const start = slice_start + block_start;
mi_assert_internal(mi_addr(start) > mi_addr(page));
page->page_start = (uint8_t*)mi_ptr_bounded_data(start, reserved * block_size); // all blocks are derived from this
// initialize page meta-data
page->reserved = (uint16_t)reserved;
page->block_size = block_size;
#if MI_FREE_IS_CHECKED
// exact for offsets and block sizes below 2^32 (see `alloc/free.c:mi_page_block_index`)
mi_assert_internal(reserved==1 || (uint64_t)reserved * block_size <= UINT32_MAX);
page->block_recip = (reserved > 1 ? (UINT64_MAX / block_size) + 1 : 1);
#endif
page->memid = memid;
page->free_is_zero = memid.initially_zero;
page->slice_pcommitted = (uint16_t)(commit_size / _mi_os_page_size());
page->heap = mi_theap_heap(theap);
#if MI_DEBUG>0
mi_atomic_store_relaxed(&page->debug_fill_preserve, mi_atomic_load_relaxed(&page->heap->debug_fill_preserve));
#endif
if (block_size <= MI_LARGE_MAX_OBJ_SIZE && mi_memtag_use_for(memid.is_memtag, block_size)) { // huge blocks are not tagged (but have an end guard page in secure mode); over-aligned singletons up to this size are tagged
mi_page_flags_set(page, true, MI_PAGE_IS_MEMTAGGED);
}
mi_page_set_theap(page,theap);
// mi_assert_internal(mi_page_theap(page) == _mi_heap_theap_peek(page->heap))
}
#if MI_PAGE_META_IS_ALIGNED
// Point the aligned page meta entries of the slices to the actual page meta data.
static void mi_arenas_page_link_aligned_meta(mi_page_t* page, uint8_t* slice_start, size_t slice_count, size_t max_page_meta_count)
{
mi_page_t* const page_meta = mi_aligned_ptr_page0(slice_start);
mi_atomic_store_ptr_release(mi_page_t,&mi_page_meta_load(page_meta)->self,page);
if (slice_count > 1) {
// at least two for large singleton blocks as guard pages can have a large offset beyond a single slice
// (note: the entries of adjacent slices are not adjacent, see `mi_page_meta_aligned_index`)
for(size_t i = 1; i < max_page_meta_count; i++) {
mi_page_t* const meta = mi_page_meta_load(mi_aligned_ptr_page0(slice_start + _mi_size_of_slices(i)));
mi_assert_internal(meta->block_size == 0);
mi_atomic_store_ptr_release(mi_page_t,&meta->self,page);
}
}
#if MI_DEBUG>1
mi_page_t* pstart = mi_page_meta_load(mi_aligned_ptr_page0(slice_start));
mi_assert_internal(mi_atomic_load_ptr_acquire(mi_page_t,&pstart->self)==page);
if (page->reserved>1) {
mi_page_t* pend = mi_page_meta_load(mi_aligned_ptr_page0(slice_start + (slice_count*MI_ARENA_SLICE_SIZE) - 1));
mi_assert_internal(mi_atomic_load_ptr_acquire(mi_page_t,&pend->self)==page);
}
#endif
}
#endif
// Register a fresh page in its arena pages and the page map, and claim ownership.
// On failure the page is freed.
static bool mi_arenas_page_register(mi_theap_t* theap, mi_page_t* page, mi_arena_pages_t* arena_pages)
{
// register in the arena_pages
if (arena_pages!=NULL) {
mi_assert_internal(page->memid.memkind == MI_MEM_ARENA);
mi_bitmap_set(arena_pages->pages, page->memid.mem.arena.slice_index);
}
// and own it
mi_page_claim_ownership(page);
// register in the page map
if mi_unlikely(!_mi_page_map_register(page)) {
_mi_arenas_page_free_prim(page);
return false;
}
// stats
mi_theap_stat_increase(theap, pages, 1);
mi_theap_stat_increase(theap, page_bins[_mi_page_stats_bin(page)], 1);
return true;
}
// Allocate a fresh page with the given guard pages (see `mi_theap_page_guards`)
static mi_page_t* mi_arenas_page_alloc_fresh(mi_theap_t* theap, size_t slice_count, size_t block_size, size_t block_alignment, bool commit, mi_page_guards_t guards)
{
const bool os_align = (block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN);
const bool singleton = (os_align || block_size > MI_LARGE_MAX_OBJ_SIZE);
const size_t max_page_meta_count = (singleton && slice_count > 2 ? 2 : slice_count);
const bool allow_large = (guards.front == 0 && guards.end == 0); // large OS pages cannot be protected
// allocate the slices (always committed with a front guard page, see `mi_arenas_page_commit_first_block`)
mi_memid_t memid = _mi_memid_none();
mi_arena_pages_t* arena_pages = NULL;
mi_page_t* page_meta = NULL;
uint8_t* const slice_start = mi_arenas_page_alloc_fresh_area(theap,slice_count,max_page_meta_count,block_alignment,os_align,commit || guards.front > 0,allow_large,&memid,&arena_pages,&page_meta);
if (!slice_start) return NULL;
// in a torn forked child, the free slices may still have a guard page of a page (or guarded block) that was freed
// during the fork (as the fork may not copy the memory protection atomically with the memory contents)
#if MI_SECURE >= 1 || MI_GUARDED
if (memid.memkind == MI_MEM_ARENA && !memid.is_pinned && _mi_fork_is_torn()) {
_mi_os_unguard(slice_start, _mi_size_of_slices(slice_count), true /* make accessible */);
}
#endif
// determine the page meta data, the offset of the first block, and the end of the page
size_t block_start = 0;
mi_page_t* page = mi_page_meta_select(slice_start, slice_count, page_meta, block_size, os_align, &guards, &block_start);
const size_t alloc_size = _mi_size_of_slices(slice_count);
const size_t reserved = (os_align ? 1 : (alloc_size - guards.end - block_start) / block_size);
const size_t page_end = (guards.end > 0 ? mi_align_up(block_start + reserved*block_size, _mi_os_page_size()) : alloc_size); // the end guard is right after the blocks
mi_assert_internal(page_end + guards.end <= alloc_size);
memid.guard_front = (guards.front > 0);
memid.guard_end = (guards.end > 0);
mi_assert_internal(block_size < MI_MAX_ALIGN_SIZE || block_start % MI_MAX_ALIGN_SIZE == 0);
// all blocks must be naturally aligned (as relied upon by `mi_malloc_aligned`)
mi_assert_internal(block_size % mi_natural_alignment_of_size(block_size) == 0);
mi_assert_internal(mi_is_aligned(slice_start + block_start, mi_natural_alignment_of_size(block_size)));
// on CHERI, all blocks must have exactly representable bounds
mi_assert_internal(mi_bounds_size(block_size) == block_size && mi_is_aligned(slice_start + block_start, mi_bounds_align(block_size)));
// commit the first block (if needed)
size_t commit_size = 0;
if (!mi_arenas_page_commit_first_block(theap, memid, slice_start, block_start, block_size, page_end, &commit_size)) {
_mi_arenas_free(mi_theap_subproc(theap), slice_start, alloc_size, memid);
return NULL;
}
mi_assert_internal((commit && commit_size==0) || (!commit && (commit_size <= UINT16_MAX * _mi_os_page_size())));
// initialize the page memory and meta data
mi_arenas_page_init_memory(theap, page, slice_start, slice_count, block_start, page_end, &memid);
page = mi_page_meta_memtag(page, slice_start, memid);
mi_arenas_page_init_meta(theap, page, slice_start, block_start, block_size, reserved, commit_size, memid);
#if MI_PAGE_META_IS_ALIGNED
mi_arenas_page_link_aligned_meta(page, slice_start, slice_count, max_page_meta_count);
#endif
mi_assert_internal(page->free==NULL);
mi_assert_internal((mi_addr(page) != mi_addr(slice_start)) == mi_page_meta_is_separated(page));
mi_assert_internal(mi_page_slice_start(page) == slice_start);
mi_assert_internal(mi_page_slice_offset_of(page, mi_page_size(page)) <= page_end);
mi_assert_internal(!memid.guard_end || mi_page_guard_end_offset(page) == page_end);
// now we can register the page (and use `_mi_arenas_page_free_prim` on error)
if (!mi_arenas_page_register(theap, page, arena_pages)) return NULL;
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page),MI_PAGE_ALIGN));
mi_assert_internal(mi_ptr_page(mi_page_start(page))==page);
mi_assert_internal(mi_page_block_size(page) == block_size);
#if MI_PAGE_META_SMALL_IS_ALIGNED
// `mi_free_small` finds the page meta data by aligning a block pointer down to `MI_SMALL_PAGE_SIZE`
if (mi_page_is_small(page)) {
mi_assert_internal(mi_addr(page) == mi_addr(slice_start) && mi_is_aligned(page, MI_SMALL_PAGE_SIZE));
mi_assert_internal(mi_addr(mi_page_start(page)) + mi_page_size(page) <= mi_addr(page) + MI_SMALL_PAGE_SIZE);
}
#endif
// mi_assert_internal(mi_page_is_abandoned(page));
mi_assert_internal(mi_page_is_owned(page));
return page;
}
/* -----------------------------------------------------------
Allocate page (`_mi_arenas_page_alloc`)
----------------------------------------------------------- */
// Allocate a regular small/medium/large page.
static mi_page_t* mi_arenas_page_regular_alloc(mi_theap_t* theap, size_t slice_count, size_t block_size)
{
// 1. look for an abandoned page
const mi_page_guards_t guards = mi_theap_page_guards(theap, _mi_size_of_slices(slice_count), false, false);
mi_page_t* page = _mi_arenas_page_try_find_abandoned(theap, slice_count, block_size, guards);
if (page != NULL) {
return page; // return as abandoned
}
// 2. find a free block, potentially allocating a new arena
const long commit_on_demand = mi_option_get(mi_option_page_commit_on_demand);
const bool commit = (mi_page_min_commit_size() >= slice_count * MI_ARENA_SLICE_SIZE || // always commit small pages
(slice_count >= _mi_slice_count_of_size(UINT16_MAX * _mi_os_page_size())) || // always commit pages too large to hold a 32-bit slice_committed
(commit_on_demand == 2 && _mi_os_has_overcommit()) || (commit_on_demand == 0));
page = mi_arenas_page_alloc_fresh(theap, slice_count, block_size, 1, commit, guards);
if (page == NULL) return NULL;
mi_assert_internal(page->memid.memkind != MI_MEM_ARENA || page->memid.mem.arena.slice_count == slice_count);
if (!_mi_page_init(theap, page)) {
_mi_arenas_page_free_direct(page,theap);
return NULL;
}
return page;
}
// Allocate a page containing one block (very large, or with large alignment)
static mi_page_t* mi_arenas_page_singleton_alloc(mi_theap_t* theap, size_t block_size, size_t block_alignment)
{
const bool os_align = (block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN);
#if MI_PAGE_META_OS_IS_SEPARATED
const size_t info_size = 0;
#else
const size_t info_size = (os_align ? MI_PAGE_ALIGN : mi_align_up(mi_page_info_size(), mi_bounds_align(block_size))); // see `mi_page_meta_in_front_block_start`
#endif
// room for the cache offset of the block (committed but not touched)
const size_t cache_offset_size = (mi_page_block_can_cache_offset(block_size, os_align) ? MI_PAGE_CACHE_OFFSET_MAX : 0);
// determine the guard pages based on the page size without guard pages
const size_t size = info_size + cache_offset_size + block_size;
const mi_page_guards_t guards = mi_theap_page_guards(theap, _mi_size_of_slices(_mi_slice_count_of_size(size)), true, os_align);
// with memory tagging, a singleton without cache offset or end guard page may fill its slices exactly and share its
// tag class with the first block of an adjacent page; reserve a tag 0 unit at the end (see `mi_page_memtag_parity`)
#if MI_MTE
const size_t memtag_slack = (cache_offset_size == 0 && guards.end == 0 ? MI_MEMTAG_UNIT_SIZE : 0);
#else
const size_t memtag_slack = 0;
#endif
const size_t slice_count = _mi_slice_count_of_size(mi_page_guard_front_block_start(guards.front, block_size) + size + memtag_slack + guards.end);
mi_page_t* page = mi_arenas_page_alloc_fresh(theap, slice_count, block_size, block_alignment, true /* commit singletons always */, guards);
if (page == NULL) return NULL;
mi_assert(page->reserved == 1);
if (!_mi_page_init(theap, page)) {
_mi_arenas_page_free_direct(page,theap);
return NULL;
}
return page;
}
mi_page_t* _mi_arenas_page_alloc(mi_theap_t* theap, size_t block_size, size_t block_alignment) {
mi_page_t* page;
// semi static assert: ensure that all non-singleton block size bins are covered.
mi_assert(_mi_bin(MI_LARGE_MAX_OBJ_SIZE) < MI_ARENA_BIN_COUNT);
if mi_unlikely(block_alignment > MI_PAGE_MAX_OVERALLOC_ALIGN) {
mi_assert_internal(mi_is_power_of_two(block_alignment));
page = mi_arenas_page_singleton_alloc(theap, block_size, block_alignment);
}
else if (block_size <= MI_SMALL_MAX_OBJ_SIZE) {
page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_SMALL_PAGE_SIZE), block_size);
}
else if (mi_block_size_is_medium(block_size)) {
page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_MEDIUM_PAGE_SIZE), block_size);
}
#if MI_ENABLE_LARGE_PAGES
else if (block_size <= MI_LARGE_MAX_OBJ_SIZE) {
page = mi_arenas_page_regular_alloc(theap, _mi_slice_count_of_size(MI_LARGE_PAGE_SIZE), block_size);
}
#endif
else {
page = mi_arenas_page_singleton_alloc(theap, block_size, block_alignment);
}
if mi_unlikely(page == NULL) {
return NULL;
}
// mi_assert_internal(page == NULL || _mi_page_segment(page)->subproc == tld->subproc);
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN));
mi_assert_internal(mi_ptr_page(mi_page_start(page))==page);
mi_assert_internal(block_alignment <= MI_PAGE_MAX_OVERALLOC_ALIGN || mi_is_aligned(mi_page_start(page), block_alignment));
return page;
}
/* -----------------------------------------------------------
Free a page
----------------------------------------------------------- */
static size_t mi_page_full_size(mi_page_t* page) {
if (page->memid.memkind == MI_MEM_ARENA) {
return page->memid.mem.arena.slice_count * MI_ARENA_SLICE_SIZE;
}
else if (mi_memid_is_os(page->memid)) {
// note: the page meta data may be in front of the slices (see `mi_arenas_page_alloc_from_os`)
uint8_t* const slice_start = mi_page_slice_start(page);
mi_assert_internal((uint8_t*)page->memid.mem.os.base <= slice_start);
const size_t presize = (size_t)(slice_start - (uint8_t*)page->memid.mem.os.base);
const size_t os_size = mi_memid_os_size(page->memid);
mi_assert_internal(os_size >= presize);
return (presize > os_size ? 0 : os_size - presize);
}
else {
return 0;
}
}
void _mi_arenas_page_free_prim(mi_page_t* page) {
mi_assert_internal(_mi_page_qsbr_is_clear(page));
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN));
mi_assert_internal(mi_ptr_page(mi_page_start(page))==page);
mi_assert_internal(mi_page_is_owned(page));
mi_assert_internal(mi_page_all_free(page));
mi_assert_internal(page->next==NULL && page->prev==NULL);
mi_subproc_t* const subproc = mi_page_subproc(page); // read before the page leaves its heap (as the heap may be deleted concurrently)
mi_arena_t* arena = NULL;
mi_arena_pages_t* arena_pages = NULL;
size_t slice_index = 0;
size_t slice_count = 0;
if (page->memid.memkind == MI_MEM_ARENA) {
arena = _mi_page_arena_pages(page, &slice_index, &slice_count, &arena_pages);
mi_assert_internal(arena_pages!=NULL);
mi_assert_internal(arena->subproc == subproc);
#if MI_DEBUG>1
if (!mi_page_is_full(page)) {
size_t bin = _mi_bin(mi_page_block_size(page));
mi_assert_internal(mi_bbitmap_is_clearN(arena->slices_free, slice_index, slice_count));
mi_assert_internal(mi_page_slice_committed(page) > 0 || mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count));
mi_assert_internal(bin >= MI_ARENA_BIN_COUNT || mi_bitmap_is_clearN(arena_pages->pages_abandoned[bin], slice_index, 1));
// note: we cannot check for `!mi_page_is_abandoned_and_mapped` since that may
// be (temporarily) not true if the free happens while trying to reclaim
// see `mi_arena_try_claim_abandoned`
}
#endif
// first remove the page from its heap; this waits if moving heap pages temporarily
// cleared the bit to safely claim the page (see `mi_heap_move_page_at`)
mi_bitmap_clear_once_set(arena->subproc, arena_pages->pages, slice_index);
}
// unregister page
_mi_page_map_unregister(page);
// reset the memory tags of the blocks so free memory is untagged again
if (mi_page_is_memtag(page)) {
mi_memtag_clear(mi_page_start(page), page->capacity * mi_page_block_size(page));
}
// reset the guard pages
// we must do this since we may later allocate large spans over this page and cannot have a guard page in between
// (for a page committed on demand the end guard page stays decommitted and its commit bit is clear)
uint8_t* const slice_start = mi_page_slice_start(page);
if (page->memid.guard_front) { _mi_os_secure_guard_page_reset_at(subproc, slice_start, page->memid); }
if (page->memid.guard_end) { _mi_os_secure_guard_page_reset_at(subproc, slice_start + mi_page_guard_end_offset(page), page->memid); }
// and free
mi_memid_t memid = page->memid;
const size_t slice_committed = mi_page_slice_committed(page);
if (arena != NULL) {
if (slice_committed > 0) {
// if committed on-demand, set the commit bits to account commit properly
mi_assert_internal(mi_page_full_size(page) >= slice_committed);
const size_t total_slices = slice_committed / MI_ARENA_SLICE_SIZE; // conservative
//mi_assert_internal(mi_bitmap_is_clearN(arena->slices_committed, slice_index, total_slices));
mi_assert_internal(slice_count >= total_slices);
if (total_slices > 0) {
mi_bitmap_setN(arena->slices_committed, slice_index, total_slices, NULL);
}
// any left over?
const size_t extra = slice_committed % MI_ARENA_SLICE_SIZE;
if (extra > 0) {
// pretend it was decommitted already
mi_subproc_stat_decrease(arena->subproc, committed, extra);
}
}
else {
mi_assert_internal(mi_bitmap_is_setN(arena->slices_committed, slice_index, slice_count));
}
}
else {
mi_heap_stat_decrease(page->heap, pages_os_allocated, 1);
mi_atomic_sub_acq_rel(&page->heap->os_page_count, 1); // last access to the heap (see `mi_heap_move_pages`)
if (slice_committed > 0) { mi_memid_os_set_committed(&memid, slice_committed); } // committed on-demand
}
if (mi_page_meta_is_separated(page)) { page->block_size = 0; } // for assertion checking
const size_t full_size = mi_page_full_size(page);
mi_page_meta_memtag_done(page, memid); // the page meta data cannot be accessed anymore
_mi_arenas_free(subproc, slice_start, full_size, memid);
}
// Physically release an empty page whose QSBR state has already been cleared.
void _mi_arenas_page_free_direct(mi_page_t* page, mi_theap_t* current_theapx) {
mi_assert_internal(_mi_page_qsbr_is_clear(page));
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN));
mi_assert_internal(mi_ptr_page(mi_page_start(page))==page);
mi_assert_internal(mi_page_is_owned(page));
mi_assert_internal(mi_page_all_free(page));
mi_assert_internal(mi_page_is_abandoned(page));
mi_assert_internal(page->next==NULL && page->prev==NULL);
mi_assert_internal(mi_theap_matches_thread(current_theapx));
mi_heap_t* const heap = mi_page_heap(page);
mi_theapx_stat_decrease(heap, current_theapx, page_bins[_mi_page_stats_bin(page)], 1);
mi_theapx_stat_decrease(heap, current_theapx, pages, 1);
_mi_page_free_collect(page,false); // update used count for cross-thread free's
_mi_page_update_stats(page); // and update the stats
_mi_arenas_page_free_prim(page);
}
// Free an empty page that was removed from the abandoned registries, or retain it if it is delayed by QSBR:
// a delayed singleton goes to the heap QSBR delayed list, and other pages are abandoned (to be reclaimed or collected later).
// Returns true if the page was freed.
bool _mi_arenas_page_free(mi_page_t* page, mi_theap_t* current_theapx) {
mi_assert_internal(mi_page_is_owned(page) && mi_page_all_free(page));
mi_assert_internal(mi_page_is_abandoned(page) && !mi_page_is_abandoned_mapped(page));
mi_assert_internal(page->next == NULL && page->prev == NULL);
if (_mi_page_qsbr_should_delay(page)) {
#if MI_QSBR
if (mi_page_is_singleton(page)) {
_mi_heap_qsbr_delayed_enqueue(page);
return false;
}
#endif
_mi_arenas_page_abandon(page, current_theapx);
return false;
}
_mi_page_qsbr_clear(page);
_mi_arenas_page_free_direct(page, current_theapx);
return true;
}