Skip to content

Commit

Permalink
Add double free detection in thread cache for debug build
Browse files Browse the repository at this point in the history
Add new runtime option `debug_double_free_max_scan` that specifies the max
number of stack entries to scan in the cache bit when trying to detect the
double free bug (currently debug build only).
  • Loading branch information
izaitsevfb authored and interwq committed Aug 4, 2022
1 parent adc70c0 commit 36366f3
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 9 deletions.
34 changes: 34 additions & 0 deletions include/jemalloc/internal/cache_bin.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define JEMALLOC_INTERNAL_CACHE_BIN_H

#include "jemalloc/internal/ql.h"
#include "jemalloc/internal/safety_check.h"
#include "jemalloc/internal/sz.h"

/*
Expand Down Expand Up @@ -427,6 +428,35 @@ cache_bin_full(cache_bin_t *bin) {
return ((uint16_t)(uintptr_t)bin->stack_head == bin->low_bits_full);
}

/*
* Scans the allocated area of the cache_bin for the given pointer up to limit.
* Fires safety_check_fail if the ptr is found and returns true.
*/
JEMALLOC_ALWAYS_INLINE bool
cache_bin_dalloc_safety_checks(cache_bin_t *bin, void *ptr) {
if (!config_debug || opt_debug_double_free_max_scan == 0) {
return false;
}

cache_bin_sz_t ncached = cache_bin_ncached_get_internal(bin, false);
unsigned max_scan = opt_debug_double_free_max_scan < ncached
? opt_debug_double_free_max_scan
: ncached;

void **cur = bin->stack_head;
void **limit = cur + max_scan;
for (; cur < limit; cur++) {
if (*cur == ptr) {
safety_check_fail(
"Invalid deallocation detected: double free of "
"pointer %p\n",
ptr);
return true;
}
}
return false;
}

/*
* Free an object into the given bin. Fails only if the bin is full.
*/
Expand All @@ -436,6 +466,10 @@ cache_bin_dalloc_easy(cache_bin_t *bin, void *ptr) {
return false;
}

if (unlikely(cache_bin_dalloc_safety_checks(bin, ptr))) {
return true;
}

bin->stack_head--;
*bin->stack_head = ptr;
cache_bin_assert_earlier(bin, bin->low_bits_full,
Expand Down
1 change: 1 addition & 0 deletions include/jemalloc/internal/jemalloc_internal_externs.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extern malloc_init_t malloc_init_state;
extern const char *zero_realloc_mode_names[];
extern atomic_zu_t zero_realloc_count;
extern bool opt_cache_oblivious;
extern unsigned opt_debug_double_free_max_scan;

/* Escape free-fastpath when ptr & mask == 0 (for sanitization purpose). */
extern uintptr_t san_cache_bin_nonfast_mask;
Expand Down
2 changes: 2 additions & 0 deletions include/jemalloc/internal/safety_check.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef JEMALLOC_INTERNAL_SAFETY_CHECK_H
#define JEMALLOC_INTERNAL_SAFETY_CHECK_H

#define SAFETY_CHECK_DOUBLE_FREE_MAX_SCAN_DEFAULT 32

void safety_check_fail_sized_dealloc(bool current_dealloc, const void *ptr,
size_t true_size, size_t input_size);
void safety_check_fail(const char *format, ...);
Expand Down
7 changes: 6 additions & 1 deletion src/ctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ CTL_PROTO(config_xmalloc)
CTL_PROTO(opt_abort)
CTL_PROTO(opt_abort_conf)
CTL_PROTO(opt_cache_oblivious)
CTL_PROTO(opt_debug_double_free_max_scan)
CTL_PROTO(opt_trust_madvise)
CTL_PROTO(opt_confirm_conf)
CTL_PROTO(opt_hpa)
Expand Down Expand Up @@ -479,7 +480,9 @@ static const ctl_named_node_t opt_node[] = {
{NAME("prof_sys_thread_name"), CTL(opt_prof_sys_thread_name)},
{NAME("prof_time_resolution"), CTL(opt_prof_time_res)},
{NAME("lg_san_uaf_align"), CTL(opt_lg_san_uaf_align)},
{NAME("zero_realloc"), CTL(opt_zero_realloc)}
{NAME("zero_realloc"), CTL(opt_zero_realloc)},
{NAME("debug_double_free_max_scan"),
CTL(opt_debug_double_free_max_scan)}
};

static const ctl_named_node_t tcache_node[] = {
Expand Down Expand Up @@ -2128,6 +2131,8 @@ CTL_RO_CONFIG_GEN(config_xmalloc, bool)
CTL_RO_NL_GEN(opt_abort, opt_abort, bool)
CTL_RO_NL_GEN(opt_abort_conf, opt_abort_conf, bool)
CTL_RO_NL_GEN(opt_cache_oblivious, opt_cache_oblivious, bool)
CTL_RO_NL_GEN(opt_debug_double_free_max_scan,
opt_debug_double_free_max_scan, unsigned)
CTL_RO_NL_GEN(opt_trust_madvise, opt_trust_madvise, bool)
CTL_RO_NL_GEN(opt_confirm_conf, opt_confirm_conf, bool)

Expand Down
11 changes: 11 additions & 0 deletions src/jemalloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ fxp_t opt_narenas_ratio = FXP_INIT_INT(4);

unsigned ncpus;

unsigned opt_debug_double_free_max_scan =
SAFETY_CHECK_DOUBLE_FREE_MAX_SCAN_DEFAULT;

/* Protects arenas initialization. */
malloc_mutex_t arenas_lock;

Expand Down Expand Up @@ -1420,6 +1423,10 @@ malloc_conf_init_helper(sc_data_t *sc_data, unsigned bin_shard_sizes[SC_NBINS],
CONF_HANDLE_UNSIGNED(opt_lg_tcache_flush_large_div,
"lg_tcache_flush_large_div", 1, 16,
CONF_CHECK_MIN, CONF_CHECK_MAX, /* clip */ true)
CONF_HANDLE_UNSIGNED(opt_debug_double_free_max_scan,
"debug_double_free_max_scan", 0, UINT_MAX,
CONF_DONT_CHECK_MIN, CONF_DONT_CHECK_MAX,
/* clip */ false)

/*
* The runtime option of oversize_threshold remains
Expand Down Expand Up @@ -1737,6 +1744,10 @@ malloc_conf_init_check_deps(void) {
"prof_final.\n");
return true;
}
/* To emphasize in the stats output that opt is disabled when !debug. */
if (!config_debug) {
opt_debug_double_free_max_scan = 0;
}

return false;
}
Expand Down
1 change: 1 addition & 0 deletions src/stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@ stats_general_print(emitter_t *emitter) {
OPT_WRITE_SIZE_T("tcache_gc_delay_bytes")
OPT_WRITE_UNSIGNED("lg_tcache_flush_small_div")
OPT_WRITE_UNSIGNED("lg_tcache_flush_large_div")
OPT_WRITE_UNSIGNED("debug_double_free_max_scan")
OPT_WRITE_CHAR_P("thp")
OPT_WRITE_BOOL("prof")
OPT_WRITE_CHAR_P("prof_prefix")
Expand Down
49 changes: 41 additions & 8 deletions test/unit/double_free.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ void fake_abort(const char *message) {
}

void
test_large_double_free_pre(void) {
test_double_free_pre(void) {
safety_check_set_abort(&fake_abort);
fake_abort_called = false;
}

void
test_large_double_free_post() {
test_double_free_post() {
expect_b_eq(fake_abort_called, true, "Double-free check didn't fire.");
safety_check_set_abort(NULL);
}
Expand All @@ -29,7 +29,7 @@ TEST_BEGIN(test_large_double_free_tcache) {
*/
test_skip_if(config_debug);

test_large_double_free_pre();
test_double_free_pre();
char *ptr = malloc(SC_LARGE_MINCLASS);
bool guarded = extent_is_guarded(tsdn_fetch(), ptr);
free(ptr);
Expand All @@ -44,15 +44,15 @@ TEST_BEGIN(test_large_double_free_tcache) {
fake_abort_called = true;
}
mallctl("thread.tcache.flush", NULL, NULL, NULL, 0);
test_large_double_free_post();
test_double_free_post();
}
TEST_END

TEST_BEGIN(test_large_double_free_no_tcache) {
test_skip_if(!config_opt_safety_checks);
test_skip_if(config_debug);

test_large_double_free_pre();
test_double_free_pre();
char *ptr = mallocx(SC_LARGE_MINCLASS, MALLOCX_TCACHE_NONE);
bool guarded = extent_is_guarded(tsdn_fetch(), ptr);
dallocx(ptr, MALLOCX_TCACHE_NONE);
Expand All @@ -66,12 +66,45 @@ TEST_BEGIN(test_large_double_free_no_tcache) {
*/
fake_abort_called = true;
}
test_large_double_free_post();
test_double_free_post();
}
TEST_END

TEST_BEGIN(test_small_double_free_tcache) {
test_skip_if(!config_debug);

test_skip_if(opt_debug_double_free_max_scan == 0);

bool tcache_enabled;
size_t sz = sizeof(tcache_enabled);
assert_d_eq(
mallctl("thread.tcache.enabled", &tcache_enabled, &sz, NULL, 0), 0,
"Unexpected mallctl failure");
test_skip_if(!tcache_enabled);

test_double_free_pre();
char *ptr = malloc(1);
bool guarded = extent_is_guarded(tsdn_fetch(), ptr);
free(ptr);
if (!guarded) {
free(ptr);
} else {
/*
* Skip because guarded extents may unguard immediately on
* deallocation, in which case the second free will crash before
* reaching the intended safety check.
*/
fake_abort_called = true;
}
mallctl("thread.tcache.flush", NULL, NULL, NULL, 0);
test_double_free_post();
}
TEST_END

int
main(void) {
return test(test_large_double_free_no_tcache,
test_large_double_free_tcache);
return test(
test_large_double_free_no_tcache,
test_large_double_free_tcache,
test_small_double_free_tcache);
}
1 change: 1 addition & 0 deletions test/unit/mallctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ TEST_BEGIN(test_mallctl_opt) {
TEST_MALLCTL_OPT(bool, prof_stats, prof);
TEST_MALLCTL_OPT(bool, prof_sys_thread_name, prof);
TEST_MALLCTL_OPT(ssize_t, lg_san_uaf_align, uaf_detection);
TEST_MALLCTL_OPT(unsigned, debug_double_free_max_scan, always);

#undef TEST_MALLCTL_OPT
}
Expand Down

0 comments on commit 36366f3

Please sign in to comment.