blob: 9ff35dfbe17fb5d55cbeca84f54f5a8c8b7a0046 [file] [edit]
/* ----------------------------------------------------------------------------
Copyright (c) 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.
-----------------------------------------------------------------------------*/
// Test the memory protections in secure mode (`MI_SECURE`): each test accesses protected memory
// in a forked child process that is expected to fault (so these tests are skipped on Windows).
// See `MI_SECURE_STATIC_SEALED` in `heap/process.h` and `mi_theap_page_guards` in `arena/arena-page-alloc.c`.
#include "mimalloc.h"
#include "mimalloc/alloc/page.h"
#include "mimalloc/heap/process.h"
#include "mimalloc/prim/os.h"
#include "testhelper.h"
#if !defined(_WIN32) && !MI_TRACK_ENABLED
#define MI_TEST_FAULTS (1)
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#else
#define MI_TEST_FAULTS (0)
#endif
#if MI_SECURE_STATIC_SEALED && !MI_SECURE_NO_STATIC_GUARD
#define MI_TEST_STATIC_GUARD (1)
#else
#define MI_TEST_STATIC_GUARD (0)
#endif
// Run a test only if the condition holds (and skip it otherwise)
#define CHECK_IF(name,cond,reason,expr) \
fprintf(stderr,"test: %s... ", name ); \
errno = 0; \
for(bool done = (!(cond) && check_skip(reason)), result = true; !done; done = check_result(result,name,__FILE__,__LINE__)) { result = (expr); }
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
#if MI_TEST_FAULTS
// Return `true` if reading (or writing) at `p` faults in a child process.
static bool test_access_faults(void* p, bool write) {
fflush(NULL);
const pid_t pid = fork();
if (pid == 0) {
volatile uint8_t* const q = (volatile uint8_t*)p;
const uint8_t x = *q;
if (write) { *q = x; }
_exit(0);
}
int status = 0;
return (pid > 0 && waitpid(pid, &status, 0) == pid && WIFSIGNALED(status) && (WTERMSIG(status) == SIGSEGV || WTERMSIG(status) == SIGBUS));
}
#else
static bool test_access_faults(void* p, bool write) {
MI_UNUSED(p); MI_UNUSED(write);
return true;
}
#endif
// Can the static process data be protected? (it cannot if the OS page size is larger than expected; this warns at startup)
static bool static_pages_fit(void) {
return ((MI_OS_PROBABLE_PAGE_SIZE % _mi_os_page_size()) == 0);
}
// ---------------------------------------------------------------------------
// Static process data
// ---------------------------------------------------------------------------
// The sealed process data is read-only
static bool test_sealed_readonly(void) {
if (!mi_process_get_sealed_once()->is_protected) return false;
return (test_access_faults(mi_process_get_sealed_once(), true) &&
test_access_faults(mi_process_get_sealed(), true));
}
// The static process data is surrounded by guard pages
static bool test_static_guard_front(void) {
return test_access_faults((uint8_t*)mi_process_get_sealed_once() - 1, false);
}
static bool test_static_guard_end(void) {
return test_access_faults(mi_align_up_ptr(&_mi_process.guard_end, _mi_os_page_size()), false);
}
// ---------------------------------------------------------------------------
// Huge blocks (and with `MI_SECURE=FULL` all mimalloc pages) are followed by a guard page
// (at the end of the mimalloc page, so a linear overflow faults before reaching other data)
// ---------------------------------------------------------------------------
static bool test_page_guard(size_t size, size_t alignment) {
uint8_t* const p = (uint8_t*)(alignment == 0 ? mi_malloc(size) : mi_malloc_aligned(size, alignment));
if (p == NULL) return false;
const size_t usable = mi_usable_size(p);
for (size_t i = 0; i < usable; i++) { p[i] = 0; } // all usable memory is accessible (note: `memset` is rejected by `_FORTIFY_SOURCE` beyond `size`)
const mi_page_t* const page = mi_ptr_page(p);
uint8_t* const guard = mi_page_slice_start(page) + mi_page_guard_end_offset(page);
const bool ok = (usable >= size && page->memid.guard_end && guard >= p + usable && test_access_faults(guard, false));
mi_free(p);
return ok;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main(void) {
mi_free(mi_malloc(8)); // initialize
const bool faults = (MI_TEST_FAULTS != 0);
const bool static_sealed = (MI_SECURE_STATIC_SEALED && static_pages_fit());
const bool static_guard = (static_sealed && MI_TEST_STATIC_GUARD && faults);
const bool huge_guard = (MI_SECURE >= 1 && _mi_os_secure_guard_page_size() > 0 && faults);
const bool page_guard = (MI_SECURE >= 5 && huge_guard);
CHECK_IF("sealed-readonly", static_sealed, "no sealed process data", test_sealed_readonly());
CHECK_IF("static-guard-front", static_guard, "no static guard pages", test_static_guard_front());
CHECK_IF("static-guard-end", static_guard, "no static guard pages", test_static_guard_end());
CHECK_IF("huge-guard", huge_guard, "no guard pages", test_page_guard(MI_LARGE_MAX_OBJ_SIZE + 1, 0));
CHECK_IF("huge-guard-unaligned-size", huge_guard, "no guard pages", test_page_guard(4*MI_MiB + 17, 0));
CHECK_IF("huge-guard-aligned", huge_guard, "no guard pages", test_page_guard(MI_LARGE_MAX_OBJ_SIZE + 1, 1*MI_MiB));
CHECK_IF("small-page-guard", page_guard, "no guard pages per page", test_page_guard(64, 0));
CHECK_IF("medium-page-guard", page_guard, "no guard pages per page", test_page_guard(MI_SMALL_MAX_OBJ_SIZE + 1, 0));
CHECK_IF("large-page-guard", page_guard, "no guard pages per page", test_page_guard(MI_LARGE_MAX_OBJ_SIZE, 0));
return print_test_summary();
}