mirror of
https://github.com/microsoft/mimalloc.git
synced 2024-12-27 13:33:18 +08:00
enable double free and heap corruption detection in debug mode
This commit is contained in:
parent
87bdfbb9b6
commit
b052d3b731
@ -379,7 +379,7 @@ static inline bool mi_is_in_same_segment(const void* p, const void* q) {
|
||||
}
|
||||
|
||||
static inline mi_block_t* mi_block_nextx( uintptr_t cookie, const mi_block_t* block ) {
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
return (mi_block_t*)(block->next ^ cookie);
|
||||
#else
|
||||
UNUSED(cookie);
|
||||
@ -388,7 +388,7 @@ static inline mi_block_t* mi_block_nextx( uintptr_t cookie, const mi_block_t* bl
|
||||
}
|
||||
|
||||
static inline void mi_block_set_nextx(uintptr_t cookie, mi_block_t* block, const mi_block_t* next) {
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
block->next = (mi_encoded_t)next ^ cookie;
|
||||
#else
|
||||
UNUSED(cookie);
|
||||
@ -397,16 +397,15 @@ static inline void mi_block_set_nextx(uintptr_t cookie, mi_block_t* block, const
|
||||
}
|
||||
|
||||
static inline mi_block_t* mi_block_next(const mi_page_t* page, const mi_block_t* block) {
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
mi_block_t* next = mi_block_nextx(page->cookie,block);
|
||||
#if MI_SECURE >= 4
|
||||
// check if next is at least in our segment range
|
||||
// TODO: it is better to check if it is actually inside our page but that is more expensive
|
||||
// to calculate. Perhaps with a relative free list this becomes feasible?
|
||||
if (next!=NULL && !mi_is_in_same_segment(block, next)) {
|
||||
_mi_fatal_error("corrupted free list entry at %p: %zx\n", block, (uintptr_t)next);
|
||||
}
|
||||
#endif
|
||||
// check for free list corruption: is `next` at least in our segment range?
|
||||
// TODO: it is better to check if it is actually inside our page but that is more expensive
|
||||
// to calculate. Perhaps with a relative free list this becomes feasible?
|
||||
if (next!=NULL && !mi_is_in_same_segment(block, next)) {
|
||||
_mi_fatal_error("corrupted free list entry of size %zub at %p: value 0x%zx\n", page->block_size, block, (uintptr_t)next);
|
||||
next = NULL;
|
||||
}
|
||||
return next;
|
||||
#else
|
||||
UNUSED(page);
|
||||
@ -415,7 +414,7 @@ static inline mi_block_t* mi_block_next(const mi_page_t* page, const mi_block_t*
|
||||
}
|
||||
|
||||
static inline void mi_block_set_next(const mi_page_t* page, mi_block_t* block, const mi_block_t* next) {
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
mi_block_set_nextx(page->cookie,block,next);
|
||||
#else
|
||||
UNUSED(page);
|
||||
|
@ -25,24 +25,30 @@ terms of the MIT license. A copy of the license can be found in the file
|
||||
// Define MI_SECURE to enable security mitigations
|
||||
// #define MI_SECURE 1 // guard page around metadata
|
||||
// #define MI_SECURE 2 // guard page around each mimalloc page
|
||||
// #define MI_SECURE 3 // encode free lists
|
||||
// #define MI_SECURE 4 // all security enabled (checks for double free, corrupted free list and invalid pointer free)
|
||||
// #define MI_SECURE 3 // encode free lists (detect corrupted free list (buffer overflow), and invalid pointer free)
|
||||
// #define MI_SECURE 4 // experimental, may be more expensive: checks for double free.
|
||||
|
||||
#if !defined(MI_SECURE)
|
||||
#define MI_SECURE 0
|
||||
#endif
|
||||
|
||||
// Define MI_DEBUG as 1 for basic assert checks and statistics
|
||||
// set it to 2 to do internal asserts,
|
||||
// and to 3 to do extensive invariant checking.
|
||||
// Define MI_DEBUG for debug mode
|
||||
// #define MI_DEBUG 1 // basic assertion checks and statistics, check double free, corrupted free list, and invalid pointer free.
|
||||
// #define MI_DEBUG 2 // + internal assertion checks
|
||||
// #define MI_DEBUG 3 // + extensive internal invariant checking
|
||||
#if !defined(MI_DEBUG)
|
||||
#if !defined(NDEBUG) || defined(_DEBUG)
|
||||
#define MI_DEBUG 1
|
||||
#define MI_DEBUG 2
|
||||
#else
|
||||
#define MI_DEBUG 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Encoded free lists allow detection of corrupted free lists
|
||||
// and can detect buffer overflows and double `free`s.
|
||||
#if (MI_SECURE>=3 || MI_DEBUG>=1)
|
||||
#define MI_ENCODE_FREELIST 1
|
||||
#endif
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Platform specific values
|
||||
@ -117,6 +123,8 @@ terms of the MIT license. A copy of the license can be found in the file
|
||||
#error "define more bins"
|
||||
#endif
|
||||
|
||||
// The free lists use encoded next fields
|
||||
// (Only actually encodes when MI_ENCODED_FREELIST is defined.)
|
||||
typedef uintptr_t mi_encoded_t;
|
||||
|
||||
// free lists contain blocks
|
||||
@ -125,6 +133,7 @@ typedef struct mi_block_s {
|
||||
} mi_block_t;
|
||||
|
||||
|
||||
// The delayed flags are used for efficient multi-threaded free-ing
|
||||
typedef enum mi_delayed_e {
|
||||
MI_NO_DELAYED_FREE = 0,
|
||||
MI_USE_DELAYED_FREE = 1,
|
||||
@ -180,7 +189,7 @@ typedef struct mi_page_s {
|
||||
bool is_zero; // `true` if the blocks in the free list are zero initialized
|
||||
|
||||
mi_block_t* free; // list of available free blocks (`malloc` allocates from this list)
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
uintptr_t cookie; // random cookie to encode the free lists
|
||||
#endif
|
||||
size_t used; // number of blocks in use (including blocks in `local_free` and `thread_free`)
|
||||
@ -197,8 +206,8 @@ typedef struct mi_page_s {
|
||||
|
||||
// improve page index calculation
|
||||
// without padding: 10 words on 64-bit, 11 on 32-bit. Secure adds one word
|
||||
#if (MI_INTPTR_SIZE==8 && MI_SECURE>0) || (MI_INTPTR_SIZE==4 && MI_SECURE==0)
|
||||
void* padding[1]; // 12 words on 64-bit in secure mode, 12 words on 32-bit plain
|
||||
#if (MI_INTPTR_SIZE==8 && defined(MI_ENCODE_FREELIST)) || (MI_INTPTR_SIZE==4 && !defined(MI_ENCODE_FREELIST))
|
||||
void* padding[1]; // 12 words on 64-bit with cookie, 12 words on 32-bit plain
|
||||
#endif
|
||||
} mi_page_t;
|
||||
|
||||
|
47
src/alloc.c
47
src/alloc.c
@ -32,10 +32,10 @@ extern inline void* _mi_page_malloc(mi_heap_t* heap, mi_page_t* page, size_t siz
|
||||
page->free = mi_block_next(page,block);
|
||||
page->used++;
|
||||
mi_assert_internal(page->free == NULL || _mi_ptr_page(page->free) == page);
|
||||
#if (MI_DEBUG)
|
||||
#if (MI_DEBUG!=0)
|
||||
if (!page->is_zero) { memset(block, MI_DEBUG_UNINIT, size); }
|
||||
#elif (MI_SECURE)
|
||||
block->next = 0;
|
||||
#elif (MI_SECURE!=0)
|
||||
block->next = 0; // don't leak internal data
|
||||
#endif
|
||||
#if (MI_STAT>1)
|
||||
if(size <= MI_LARGE_OBJ_SIZE_MAX) {
|
||||
@ -125,10 +125,12 @@ mi_decl_allocator void* mi_zalloc(size_t size) mi_attr_noexcept {
|
||||
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Check for double free in secure mode
|
||||
// Check for double free in secure and debug mode
|
||||
// This is somewhat expensive so only enabled for secure mode 4
|
||||
// ------------------------------------------------------
|
||||
|
||||
#if MI_SECURE>=4
|
||||
#if (MI_ENCODE_FREELIST && (MI_SECURE>=4 || MI_DEBUG!=0))
|
||||
// linear check if the free list contains a specific element
|
||||
static bool mi_list_contains(const mi_page_t* page, const mi_block_t* list, const mi_block_t* elem) {
|
||||
while (list != NULL) {
|
||||
if (elem==list) return true;
|
||||
@ -137,15 +139,15 @@ static bool mi_list_contains(const mi_page_t* page, const mi_block_t* list, cons
|
||||
return false;
|
||||
}
|
||||
|
||||
static mi_decl_noinline bool mi_check_double_freex(const mi_page_t* page, const mi_block_t* block, const mi_block_t* n) {
|
||||
static mi_decl_noinline bool mi_check_is_double_freex(const mi_page_t* page, const mi_block_t* block, const mi_block_t* n) {
|
||||
size_t psize;
|
||||
uint8_t* pstart = _mi_page_start(_mi_page_segment(page), page, &psize);
|
||||
if (n == NULL || ((uint8_t*)n >= pstart && (uint8_t*)n < (pstart + psize))) {
|
||||
// Suspicious: the decoded value is in the same page (or NULL).
|
||||
// Walk the free lists to see if it is already freed
|
||||
// Walk the free lists to verify positively if it is already freed
|
||||
if (mi_list_contains(page, page->free, block) ||
|
||||
mi_list_contains(page, page->local_free, block) ||
|
||||
mi_list_contains(page, (const mi_block_t*)mi_atomic_read_ptr_relaxed(mi_atomic_cast(void*,&page->thread_free)), block))
|
||||
mi_list_contains(page, page->local_free, block) ||
|
||||
mi_list_contains(page, (const mi_block_t*)mi_atomic_read_ptr_relaxed(mi_atomic_cast(void*,&page->thread_free)), block))
|
||||
{
|
||||
_mi_fatal_error("double free detected of block %p with size %zu\n", block, page->block_size);
|
||||
return true;
|
||||
@ -154,16 +156,23 @@ static mi_decl_noinline bool mi_check_double_freex(const mi_page_t* page, const
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline bool mi_check_double_free(const mi_page_t* page, const mi_block_t* block) {
|
||||
mi_block_t* n = (mi_block_t*)(block->next ^ page->cookie);
|
||||
if (((uintptr_t)n & (MI_INTPTR_SIZE-1))==0 && // quick check
|
||||
(n==NULL || mi_is_in_same_segment(block, n)))
|
||||
static inline bool mi_check_is_double_free(const mi_page_t* page, const mi_block_t* block) {
|
||||
mi_block_t* n = mi_block_nextx(page->cookie, block); // pretend it is freed, and get the decoded first field
|
||||
if (((uintptr_t)n & (MI_INTPTR_SIZE-1))==0 && // quick check: aligned pointer?
|
||||
(n==NULL || mi_is_in_same_segment(block, n))) // quick check: in same segment or NULL?
|
||||
{
|
||||
// Suspicous: decoded value in block is in the same segment (or NULL) -- maybe a double free?
|
||||
return mi_check_double_freex(page, block, n);
|
||||
// (continue in separate function to improve code generation)
|
||||
return mi_check_is_double_freex(page, block, n);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
static inline bool mi_check_is_double_free(const mi_page_t* page, const mi_block_t* block) {
|
||||
UNUSED(page);
|
||||
UNUSED(block);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@ -171,7 +180,6 @@ static inline bool mi_check_double_free(const mi_page_t* page, const mi_block_t*
|
||||
// Free
|
||||
// ------------------------------------------------------
|
||||
|
||||
|
||||
// multi-threaded free
|
||||
static mi_decl_noinline void _mi_free_block_mt(mi_page_t* page, mi_block_t* block)
|
||||
{
|
||||
@ -258,6 +266,7 @@ static inline void _mi_free_block(mi_page_t* page, bool local, mi_block_t* block
|
||||
// and push it on the free list
|
||||
if (mi_likely(local)) {
|
||||
// owning thread can free a block directly
|
||||
if (mi_check_is_double_free(page, block)) return;
|
||||
mi_block_set_next(page, block, page->local_free);
|
||||
page->local_free = block;
|
||||
page->used--;
|
||||
@ -301,7 +310,7 @@ void mi_free(void* p) mi_attr_noexcept
|
||||
const mi_segment_t* const segment = _mi_ptr_segment(p);
|
||||
if (mi_unlikely(segment == NULL)) return; // checks for (p==NULL)
|
||||
|
||||
#if (MI_DEBUG>0)
|
||||
#if (MI_DEBUG!=0)
|
||||
if (mi_unlikely(!mi_is_in_heap_region(p))) {
|
||||
_mi_warning_message("possibly trying to free a pointer that does not point to a valid heap region: 0x%p\n"
|
||||
"(this may still be a valid very large allocation (over 64MiB))\n", p);
|
||||
@ -310,7 +319,7 @@ void mi_free(void* p) mi_attr_noexcept
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if (MI_DEBUG>0 || MI_SECURE>=4)
|
||||
#if (MI_DEBUG!=0 || MI_SECURE>=4)
|
||||
if (mi_unlikely(_mi_ptr_cookie(segment) != segment->cookie)) {
|
||||
_mi_error_message("trying to free a pointer that does not point to a valid heap space: %p\n", p);
|
||||
return;
|
||||
@ -332,9 +341,7 @@ void mi_free(void* p) mi_attr_noexcept
|
||||
if (mi_likely(tid == segment->thread_id && page->flags.full_aligned == 0)) { // the thread id matches and it is not a full page, nor has aligned blocks
|
||||
// local, and not full or aligned
|
||||
mi_block_t* block = (mi_block_t*)p;
|
||||
#if MI_SECURE>=4
|
||||
if (mi_check_double_free(page,block)) return;
|
||||
#endif
|
||||
if (mi_check_is_double_free(page,block)) return;
|
||||
mi_block_set_next(page, block, page->local_free);
|
||||
page->local_free = block;
|
||||
page->used--;
|
||||
|
@ -15,14 +15,14 @@ const mi_page_t _mi_page_empty = {
|
||||
0, false, false, false, false, 0, 0,
|
||||
{ 0 }, false,
|
||||
NULL, // free
|
||||
#if MI_SECURE
|
||||
#if MI_ENCODE_FREELIST
|
||||
0,
|
||||
#endif
|
||||
0, // used
|
||||
NULL,
|
||||
ATOMIC_VAR_INIT(0), ATOMIC_VAR_INIT(0),
|
||||
0, NULL, NULL, NULL
|
||||
#if (MI_INTPTR_SIZE==8 && MI_SECURE>0) || (MI_INTPTR_SIZE==4 && MI_SECURE==0)
|
||||
#if (MI_INTPTR_SIZE==8 && defined(MI_ENCODE_FREELIST)) || (MI_INTPTR_SIZE==4 && !defined(MI_ENCODE_FREELIST))
|
||||
, { NULL } // padding
|
||||
#endif
|
||||
};
|
||||
|
@ -290,7 +290,9 @@ mi_attr_noreturn void _mi_fatal_error(const char* fmt, ...) {
|
||||
va_start(args, fmt);
|
||||
mi_vfprintf(NULL, "mimalloc: fatal: ", fmt, args);
|
||||
va_end(args);
|
||||
exit(99);
|
||||
#if (MI_SECURE>=0)
|
||||
abort();
|
||||
#endif
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
|
@ -512,7 +512,7 @@ static mi_decl_noinline void mi_page_free_list_extend( mi_page_t* page, size_t e
|
||||
----------------------------------------------------------- */
|
||||
|
||||
#define MI_MAX_EXTEND_SIZE (4*1024) // heuristic, one OS page seems to work well.
|
||||
#if MI_SECURE
|
||||
#if (MI_SECURE>0)
|
||||
#define MI_MIN_EXTEND (8*MI_SECURE) // extend at least by this many
|
||||
#else
|
||||
#define MI_MIN_EXTEND (1)
|
||||
@ -579,7 +579,7 @@ static void mi_page_init(mi_heap_t* heap, mi_page_t* page, size_t block_size, mi
|
||||
page->block_size = block_size;
|
||||
mi_assert_internal(page_size / block_size < (1L<<16));
|
||||
page->reserved = (uint16_t)(page_size / block_size);
|
||||
#if MI_SECURE
|
||||
#ifdef MI_ENCODE_FREELIST
|
||||
page->cookie = _mi_heap_random(heap) | 1;
|
||||
#endif
|
||||
page->is_zero = page->is_zero_init;
|
||||
@ -592,7 +592,7 @@ static void mi_page_init(mi_heap_t* heap, mi_page_t* page, size_t block_size, mi
|
||||
mi_assert_internal(page->next == NULL);
|
||||
mi_assert_internal(page->prev == NULL);
|
||||
mi_assert_internal(!mi_page_has_aligned(page));
|
||||
#if MI_SECURE
|
||||
#if (MI_ENCODE_FREELIST)
|
||||
mi_assert_internal(page->cookie != 0);
|
||||
#endif
|
||||
mi_assert_expensive(mi_page_is_valid_init(page));
|
||||
|
@ -9,11 +9,16 @@
|
||||
|
||||
static void double_free1();
|
||||
static void double_free2();
|
||||
static void corrupt_free();
|
||||
|
||||
int main() {
|
||||
mi_version();
|
||||
|
||||
// detect double frees and heap corruption
|
||||
//double_free1();
|
||||
//double_free2();
|
||||
//corrupt_free();
|
||||
|
||||
void* p1 = malloc(78);
|
||||
void* p2 = malloc(24);
|
||||
free(p1);
|
||||
@ -36,9 +41,13 @@ int main() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// The double free samples come ArcHeap [1] by Insu Yun (issue #161)
|
||||
// [1]: https://arxiv.org/pdf/1903.00503.pdf
|
||||
|
||||
static void double_free1() {
|
||||
void* p[256];
|
||||
uintptr_t buf[256];
|
||||
//uintptr_t buf[256];
|
||||
|
||||
p[0] = mi_malloc(622616);
|
||||
p[1] = mi_malloc(655362);
|
||||
@ -54,7 +63,7 @@ static void double_free1() {
|
||||
|
||||
static void double_free2() {
|
||||
void* p[256];
|
||||
uintptr_t buf[256];
|
||||
//uintptr_t buf[256];
|
||||
// [INFO] Command buffer: 0x327b2000
|
||||
// [INFO] Input size: 182
|
||||
p[0] = malloc(712352);
|
||||
@ -69,3 +78,32 @@ static void double_free2() {
|
||||
// p[4]=0x433f1402000 (size=917504), p[1]=0x433f14c2000 (size=786432)
|
||||
fprintf(stderr, "p1: %p-%p, p2: %p-%p\n", p[4], (uint8_t*)(p[4]) + 917504, p[1], (uint8_t*)(p[1]) + 786432);
|
||||
}
|
||||
|
||||
|
||||
// Try to corrupt the heap through buffer overflow
|
||||
#define N 256
|
||||
#define SZ 64
|
||||
|
||||
static void corrupt_free() {
|
||||
void* p[N];
|
||||
// allocate
|
||||
for (int i = 0; i < N; i++) {
|
||||
p[i] = malloc(SZ);
|
||||
}
|
||||
// free some
|
||||
for (int i = 0; i < N; i += (N/10)) {
|
||||
free(p[i]);
|
||||
p[i] = NULL;
|
||||
}
|
||||
// try to corrupt the free list
|
||||
for (int i = 0; i < N; i++) {
|
||||
if (p[i] != NULL) {
|
||||
memset(p[i], 0, SZ+8);
|
||||
}
|
||||
}
|
||||
// allocate more.. trying to trigger an allocation from a corrupted entry
|
||||
// this may need many allocations to get there (if at all)
|
||||
for (int i = 0; i < 4096; i++) {
|
||||
malloc(SZ);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user