blob: 014faecffd2091c5d28144e22f102276167d0775 [file] [edit]
/*----------------------------------------------------------------------------
Copyright (c) 2023-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.
-----------------------------------------------------------------------------*/
#include "mimalloc.h"
#include "mimalloc/util/memid.h"
#include "mimalloc/alloc/page.h"
#include "mimalloc/arena/arena.h"
#include "mimalloc/heap/subproc.h"
#include "mimalloc/heap/page-map.h"
#include "../arena/bitmap.h"
static void mi_page_map_cannot_commit(void) {
_mi_warning_message("unable to commit the allocation page-map on-demand\n" );
}
// A 2-level page map
#define MI_PAGE_MAP_SUB_SIZE (MI_PAGE_MAP_SUB_COUNT * sizeof(mi_ptr_signed_t))
// Set a (signed) sub map pointer in the page map
static void mi_page_map_set_at(mi_page_map_t* pmap, size_t idx, mi_submap_t sub) {
mi_atomic_ptr_sign_release(&pmap->submaps[idx], sub);
}
static size_t mi_page_map_count_of_size(size_t size) {
return (size < sizeof(mi_page_map_t) ? 0 : 1 + (size - sizeof(mi_page_map_t))/sizeof(mi_ptr_signed_t));
}
static size_t mi_page_map_size_of_count(size_t count) {
return (count == 0 ? 0 : sizeof(mi_page_map_t) + (count - 1)*sizeof(mi_ptr_signed_t));
}
// With memory tagging, the page map and its sub maps get a random tag (and are only accessed through the tagged pointers).
// The page map is tagged incrementally as it is committed on-demand.
static bool mi_page_map_use_memtag(const mi_memid_t* memid) {
return mi_memtag_use_for_meta(!memid->is_pinned);
}
// static void* mi_page_map_addr_of_index(size_t idx) {
// return (void*)((uintptr_t)idx * MI_PAGE_MAP_SUB_COUNT * MI_ARENA_SLICE_SIZE);
// }
// Store the committed count in the sealed process data
static void mi_page_map_store_committed(size_t committed_count) {
mi_process_sealed_t* const sealed = _mi_process_unseal();
mi_atomic_store_release(&sealed->page_map_committed, committed_count);
_mi_process_seal();
}
// The page map with the authority to commit or free it (on CHERI), and without a memory tag
static mi_page_map_t* mi_page_map_vmem(mi_page_map_t* pmap) {
return (mi_page_map_t*)mi_ptr_memtag_meta(mi_ptr_rederive(pmap->memid.mem.os.base, pmap));
}
mi_decl_nodiscard static mi_decl_noinline bool mi_page_map_commit_entries(mi_page_map_t* pmap, size_t required_idx) {
const size_t reserved_count = mi_page_map_count_of_size(pmap->reserved_size);
if mi_unlikely(required_idx >= reserved_count) {
mi_page_map_cannot_commit();
return false;
}
size_t commit_size = mi_align_up( sizeof(mi_page_map_t) + (required_idx * sizeof(mi_ptr_signed_t)), MI_ARENA_SLICE_SIZE );
if (pmap->reserved_size < commit_size) { commit_size = pmap->reserved_size; }
const size_t commit_count = mi_page_map_count_of_size(commit_size);
mi_assert_internal(commit_count > required_idx);
mi_assert_internal(commit_count <= reserved_count);
const size_t prev_count = mi_page_map_committed();
// note: we rely on uncommitted memory to be zero initialized on the first commit (and further concurrent commits leave the memory as is).
if (!pmap->memid.initially_committed) { // can be initially committed with memory tagging (see `mi_page_map_init_once`)
bool is_zero;
if mi_unlikely(!_mi_os_commit(_mi_subproc_main(), mi_page_map_vmem(pmap), commit_size, &is_zero)) {
mi_page_map_cannot_commit();
return false;
}
mi_assert_internal(is_zero || pmap->memid.initially_zero);
}
// tag the new entries with the tag of `pmap` before publishing the new count
// (concurrent commits may tag the same entries again but that leaves the memory as is)
if (mi_page_map_use_memtag(&pmap->memid)) {
const size_t tag_start = mi_align_down(mi_page_map_size_of_count(prev_count), MI_MEMTAG_UNIT_SIZE);
mi_memtag_store((uint8_t*)pmap + tag_start, commit_size - tag_start);
}
mi_page_map_store_committed(commit_count);
return true;
}
mi_decl_nodiscard static bool mi_page_map_ensure_committed(mi_page_map_t* pmap, size_t idx, mi_submap_t* submap) {
mi_assert_internal(submap!=NULL && *submap==NULL);
if mi_unlikely(idx >= mi_page_map_committed()) {
// commit under the lock so the committed count only increases
bool ok = true;
mi_lock(&pmap->lock) {
if (idx >= mi_atomic_load_acquire(&mi_process_get_sealed()->page_map_committed)) {
ok = mi_page_map_commit_entries(pmap,idx);
}
}
if (!ok) return false;
mi_assert_internal(idx < mi_page_map_committed());
}
*submap = mi_page_map_at(pmap, idx);
return true;
}
// initialize the page map
static bool mi_page_map_init_once(void) {
size_t vbits = (size_t)mi_option_get_clamp(mi_option_max_vabits, 0, MI_MAX_VABITS);
if (vbits == 0) {
vbits = _mi_os_virtual_address_bits();
#if MI_ARCH_X64 // canonical address is limited to the first 128 TiB
if (vbits >= 48) { vbits = 47; }
#endif
}
if (vbits < MI_PAGE_MAP_SUB_SHIFT + MI_ARENA_SLICE_SHIFT) {
vbits = MI_PAGE_MAP_SUB_SHIFT + MI_ARENA_SLICE_SHIFT;
}
if (vbits < MI_MIN_VABITS) { // cover at least this much for a faster _mi_checked_ptr
vbits = MI_MIN_VABITS;
}
if (vbits > MI_MAX_VABITS) { // limit page map size even if more virtual addresses are available
vbits = MI_MAX_VABITS;
}
// Allocate the page map and commit bits
mi_assert(MI_MAX_VABITS >= vbits);
mi_assert(MI_MIN_VABITS <= vbits);
const size_t reserve_count = (MI_ZU(1) << (vbits - MI_PAGE_MAP_SUB_SHIFT - MI_ARENA_SLICE_SHIFT));
const size_t os_page_size = _mi_os_page_size();
const size_t reserve_size = mi_align_up( sizeof(mi_page_map_t) + ((reserve_count - 1) * sizeof(mi_ptr_signed_t)), os_page_size);
const size_t submap_size = MI_PAGE_MAP_SUB_SIZE;
const size_t extra_reserve_size = reserve_size + submap_size;
const bool commit = (vbits == MI_MIN_VABITS) || (reserve_size <= 64*MI_KiB) || // 42 virtual address bits
mi_option_is_enabled(mi_option_pagemap_commit) || _mi_os_has_overcommit();
mi_subproc_t* const subproc = _mi_subproc_main();
mi_memid_t memid;
mi_page_map_t* pmap = (mi_page_map_t*)_mi_os_alloc_with_guards(subproc, extra_reserve_size, commit, true /* allow large */, &memid);
if mi_unlikely(pmap==NULL) {
_mi_error_message(ENOMEM, "unable to reserve virtual memory for the page map (%zu KiB)\n", extra_reserve_size / MI_KiB);
return false;
}
// commit first entries up to MI_MIN_VABITS entries
const bool use_memtag = mi_page_map_use_memtag(&memid);
const size_t min_commit_count = (MI_ZU(1) << (MI_MIN_VABITS - MI_PAGE_MAP_SUB_SHIFT - MI_ARENA_SLICE_SHIFT));
const size_t min_commit_size = mi_align_up( mi_page_map_size_of_count(min_commit_count), os_page_size);
mi_assert_internal(min_commit_size <= reserve_size);
size_t commit_count;
if (memid.initially_committed) {
if (!memid.initially_zero) {
_mi_warning_message("internal: the page map was committed but not zero initialized!\n");
mi_memzero_aligned(pmap, extra_reserve_size);
memid.initially_zero = true;
}
// with memory tagging, we still "commit" on-demand so we only tag the used part of the page map
commit_count = mi_page_map_count_of_size(use_memtag ? min_commit_size : reserve_size);
}
else {
bool is_zero;
if (!_mi_os_commit(subproc,pmap,min_commit_size,&is_zero)) {
mi_page_map_cannot_commit();
_mi_os_free_with_guards(subproc,memid);
return false;
};
mi_assert_internal(is_zero || memid.initially_zero);
commit_count = mi_page_map_count_of_size(min_commit_size);
}
mi_assert_internal(commit_count >= min_commit_count);
// ensure there is a submap for the NULL address
mi_submap_t sub0 = (mi_submap_t)((uint8_t*)pmap + reserve_size); // we reserved a submap part at the end already
if (!memid.initially_committed) {
if (!_mi_os_commit(subproc, sub0, submap_size, NULL)) { // commit full submap (issue #1087)
mi_page_map_cannot_commit();
_mi_os_free_with_guards(subproc,memid);
return false;
}
}
if (!memid.initially_zero) { // initialize low addresses with NULL
mi_memzero_aligned(sub0, submap_size);
}
// tag the committed page map entries and the first sub map (with a different tag as it is adjacent once the page map is fully committed)
if (use_memtag) {
pmap = (mi_page_map_t*)mi_memtag_meta_init_separate(pmap, mi_align_up(mi_page_map_size_of_count(commit_count), MI_MEMTAG_UNIT_SIZE), 0, NULL);
sub0 = (mi_submap_t)mi_memtag_meta_init_separate(sub0, submap_size, 1, pmap);
}
// initialize the fields
pmap->memid = memid;
pmap->reserved_size = reserve_size;
mi_lock_init(&pmap->lock);
mi_page_map_set_at(pmap, 0, (mi_submap_t)mi_ptr_bounded_data(sub0, submap_size));
// set the page map (once) before the committed count as `mi_checked_ptr_page` only reads the page map if the count is non-zero
mi_process_sealed_once_t* const once = _mi_process_unseal_once();
mi_page_map_t* const pmap_bounded = (mi_page_map_t*)mi_ptr_bounded_data(pmap, reserve_size); // see `mi_page_map_vmem`
mi_atomic_store_ptr_release(mi_page_map_t, &once->page_map, (mi_page_map_t*)mi_ptr_memtag_meta(pmap_bounded)); // store without memory tag (see `mi_page_map`)
_mi_process_seal_once();
mi_page_map_store_committed(commit_count);
mi_assert_internal(mi_ptr_page(NULL)==NULL);
return true;
}
bool _mi_page_map_init(void) {
mi_atomic_do_once {
mi_page_map_init_once();
}
return (mi_page_map() != NULL);
}
// Sub maps are allocated with guard pages around them in secure mode
static mi_submap_t mi_page_map_alloc_submap(mi_subproc_t* subproc, mi_memid_t* memid) {
mi_submap_t sub = (mi_submap_t)_mi_os_alloc_with_guards(subproc, MI_PAGE_MAP_SUB_SIZE, true /* commit */, false /* allow large */, memid);
if (sub != NULL && !memid->initially_zero) {
mi_memzero_aligned(sub, MI_PAGE_MAP_SUB_SIZE);
}
if (sub != NULL && mi_page_map_use_memtag(memid)) {
sub = (mi_submap_t)mi_memtag_meta_init_separate(sub, MI_PAGE_MAP_SUB_SIZE, 0, NULL);
}
return sub;
}
// We do not store the memid of a sub map but can recreate it (see `_mi_os_alloc_with_guards`)
static mi_memid_t mi_page_map_submap_memid(mi_submap_t sub) {
const size_t guard_size = _mi_os_secure_guard_page_size();
const size_t size = _mi_os_good_alloc_size(MI_PAGE_MAP_SUB_SIZE + 2*guard_size);
return _mi_memid_create_os((uint8_t*)mi_ptr_memtag_meta(sub) - guard_size, size, size, false, false);
}
void _mi_page_map_unsafe_destroy(void) {
mi_page_map_t* const pmap = mi_page_map();
mi_assert_internal(pmap != NULL);
if (pmap == NULL) return;
mi_subproc_t* const subproc = _mi_subproc_main();
mi_lock_done(&pmap->lock);
const size_t committed_count = mi_page_map_committed();
mi_page_map_store_committed(0); // before freeing so `mi_checked_ptr_page` returns NULL (the page map itself is sealed and not reset)
for (size_t idx = 1; idx < committed_count; idx++) { // skip entry 0 (as we allocate that submap at the end of the page_map)
// free all sub-maps
mi_submap_t sub = mi_page_map_at(pmap,idx);
if (sub != NULL) {
_mi_os_free_with_guards(subproc, mi_page_map_submap_memid(sub));
mi_page_map_set_at(pmap, idx, NULL);
}
}
_mi_os_free_with_guards(subproc, pmap->memid);
}
mi_decl_nodiscard static mi_decl_noinline mi_submap_t mi_page_map_alloc_submap_at(mi_page_map_t* pmap, size_t idx) {
// sub map not yet allocated, alloc now
mi_submap_t sub = NULL;
mi_lock(&pmap->lock)
{
sub = mi_page_map_at(pmap, idx); // reload
if (sub==NULL) // not yet allocated by another thread?
{
mi_subproc_t* const subproc = _mi_subproc_main();
mi_memid_t memid;
sub = mi_page_map_alloc_submap(subproc, &memid);
if (sub==NULL) {
_mi_warning_message("internal error: unable to extend the page map\n");
}
else {
mi_page_map_set_at(pmap, idx, sub); // ok, since we hold the lock
}
}
}
return sub;
}
mi_decl_nodiscard static bool mi_page_map_ensure_submap_at(mi_page_map_t* pmap, size_t idx, mi_submap_t* submap) {
mi_assert_internal(submap!=NULL && *submap==NULL);
mi_submap_t sub = NULL;
if (!mi_page_map_ensure_committed(pmap, idx, &sub)) {
return false;
}
if mi_unlikely(sub==NULL) {
sub = mi_page_map_alloc_submap_at(pmap, idx);
if (sub==NULL) return false; // unable to allocate the submap..
}
mi_assert_internal(sub!=NULL);
*submap = sub;
return true;
}
static bool mi_page_map_set_range_prim(mi_page_map_t* pmap, mi_page_t* page, size_t idx, size_t sub_idx, size_t slice_count) {
// is the page map area that contains the page address committed?
while (slice_count > 0) {
mi_submap_t sub = NULL;
if (!mi_page_map_ensure_submap_at(pmap, idx, &sub)) {
return false;
};
mi_assert_internal(sub!=NULL);
// set the offsets for the page; use atomic stores as the entries of a freed OS page can be set by another
// thread that reallocates the same address range (and the OS unmap and map do not synchronize for TSAN)
while (slice_count > 0 && sub_idx < MI_PAGE_MAP_SUB_COUNT) {
mi_atomic_ptr_sign_relaxed((_Atomic(mi_ptr_signed_t)*)&sub[sub_idx], page);
slice_count--;
sub_idx++;
}
idx++; // potentially wrap around to the next idx
sub_idx = 0;
}
return true;
}
static bool mi_page_map_set_range(mi_page_map_t* pmap, mi_page_t* page, size_t idx, size_t sub_idx, size_t slice_count) {
if mi_unlikely(!mi_page_map_set_range_prim(pmap, page,idx,sub_idx,slice_count)) {
// failed to commit, call again to reset the page pointer if needed
if (page!=NULL) {
mi_page_map_set_range_prim(pmap,NULL,idx,sub_idx,slice_count);
}
return false;
}
return true;
}
static size_t mi_page_map_get_idx(mi_page_t* page, size_t* sub_idx, size_t* slice_count) {
size_t page_size;
uint8_t* page_start = mi_page_area(page, &page_size);
if (page_size > MI_LARGE_PAGE_SIZE) { page_size = MI_LARGE_PAGE_SIZE - MI_ARENA_SLICE_SIZE; } // furthest interior pointer
mi_assert_internal(page_size > 0);
// count the slices spanned by `[page_start, page_start + page_size)` exactly
const mi_addr_t start = mi_addr(page_start);
*slice_count = ((start + page_size - 1) / MI_ARENA_SLICE_SIZE) - (start / MI_ARENA_SLICE_SIZE) + 1;
return mi_page_map_index(page_start, sub_idx);
}
bool _mi_page_map_register(mi_page_t* page) {
mi_assert_internal(page != NULL);
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN));
mi_page_map_t* pmap = mi_page_map();
mi_assert_internal(pmap != NULL); // should be initialized before multi-thread access!
if mi_unlikely(pmap == NULL) {
if (!_mi_page_map_init()) return false;
pmap = mi_page_map();
}
mi_assert(pmap!=NULL);
size_t slice_count;
size_t sub_idx;
const size_t idx = mi_page_map_get_idx(page, &sub_idx, &slice_count);
return mi_page_map_set_range(pmap, page, idx, sub_idx, slice_count);
}
void _mi_page_map_unregister(mi_page_t* page) {
mi_assert_internal(page != NULL);
mi_assert_internal(mi_is_aligned(mi_page_slice_start(page), MI_PAGE_ALIGN));
mi_page_map_t* const pmap = mi_page_map();
// note: should proceed even if the page was not registered yet (for failure paths in page allocation in `arena/arena-page-alloc.c`)
if mi_unlikely(pmap == NULL) return;
// get index and count
size_t slice_count;
size_t sub_idx;
const size_t idx = mi_page_map_get_idx(page, &sub_idx, &slice_count);
// unset the offsets
mi_page_map_set_range(pmap, NULL, idx, sub_idx, slice_count);
}
void _mi_page_map_unregister_range(void* start, size_t size) {
mi_page_map_t* const pmap = mi_page_map();
if mi_unlikely(pmap == NULL) return;
const size_t slice_count = mi_divide_up(size, MI_ARENA_SLICE_SIZE);
size_t sub_idx;
const size_t idx = mi_page_map_index(start, &sub_idx);
mi_page_map_set_range(pmap, NULL, idx, sub_idx, slice_count); // todo: avoid committing if not already committed?
}
// Return NULL for invalid pointers
mi_page_t* _mi_safe_ptr_page(const void* p) {
return mi_checked_ptr_page(p);
}
mi_decl_nodiscard mi_decl_export bool mi_is_in_heap_region(const void* p) mi_attr_noexcept {
return (_mi_safe_ptr_page(p) != NULL);
}