Implement use-after-free detection using junk and stash.
On deallocation, sampled pointers (specially aligned) get junked and stashed into tcache (to prevent immediate reuse). The expected behavior is to have read-after-free corrupted and stopped by the junk-filling, while write-after-free is checked when flushing the stashed pointers.
This commit is contained in:
@@ -157,6 +157,8 @@ arena_stats_merge(tsdn_t *tsdn, arena_t *arena, unsigned *nthreads,
|
||||
cache_bin_t *cache_bin = &descriptor->bins[i];
|
||||
astats->tcache_bytes +=
|
||||
cache_bin_ncached_get_remote(cache_bin,
|
||||
&tcache_bin_info[i]) * sz_index2size(i) +
|
||||
cache_bin_nstashed_get(cache_bin,
|
||||
&tcache_bin_info[i]) * sz_index2size(i);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
#include "jemalloc/internal/jemalloc_internal_includes.h"
|
||||
|
||||
#include "jemalloc/internal/bit_util.h"
|
||||
#include "jemalloc/internal/cache_bin.h"
|
||||
#include "jemalloc/internal/safety_check.h"
|
||||
|
||||
void
|
||||
cache_bin_info_init(cache_bin_info_t *info,
|
||||
|
@@ -150,6 +150,7 @@ CTL_PROTO(opt_prof_recent_alloc_max)
|
||||
CTL_PROTO(opt_prof_stats)
|
||||
CTL_PROTO(opt_prof_sys_thread_name)
|
||||
CTL_PROTO(opt_prof_time_res)
|
||||
CTL_PROTO(opt_lg_san_uaf_align)
|
||||
CTL_PROTO(opt_zero_realloc)
|
||||
CTL_PROTO(tcache_create)
|
||||
CTL_PROTO(tcache_flush)
|
||||
@@ -472,6 +473,7 @@ static const ctl_named_node_t opt_node[] = {
|
||||
{NAME("prof_stats"), CTL(opt_prof_stats)},
|
||||
{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)}
|
||||
};
|
||||
|
||||
@@ -2201,6 +2203,8 @@ CTL_RO_NL_CGEN(config_prof, opt_prof_sys_thread_name, opt_prof_sys_thread_name,
|
||||
bool)
|
||||
CTL_RO_NL_CGEN(config_prof, opt_prof_time_res,
|
||||
prof_time_res_mode_names[opt_prof_time_res], const char *)
|
||||
CTL_RO_NL_CGEN(config_uaf_detection, opt_lg_san_uaf_align,
|
||||
opt_lg_san_uaf_align, ssize_t)
|
||||
CTL_RO_NL_GEN(opt_zero_realloc,
|
||||
zero_realloc_mode_names[opt_zero_realloc_action], const char *)
|
||||
|
||||
|
@@ -1657,6 +1657,31 @@ malloc_conf_init_helper(sc_data_t *sc_data, unsigned bin_shard_sizes[SC_NBINS],
|
||||
}
|
||||
CONF_CONTINUE;
|
||||
}
|
||||
if (config_uaf_detection &&
|
||||
CONF_MATCH("lg_san_uaf_align")) {
|
||||
ssize_t a;
|
||||
CONF_VALUE_READ(ssize_t, a)
|
||||
if (CONF_VALUE_READ_FAIL() || a < -1) {
|
||||
CONF_ERROR("Invalid conf value",
|
||||
k, klen, v, vlen);
|
||||
}
|
||||
if (a == -1) {
|
||||
opt_lg_san_uaf_align = -1;
|
||||
CONF_CONTINUE;
|
||||
}
|
||||
|
||||
/* clip if necessary */
|
||||
ssize_t max_allowed = (sizeof(size_t) << 3) - 1;
|
||||
ssize_t min_allowed = LG_PAGE;
|
||||
if (a > max_allowed) {
|
||||
a = max_allowed;
|
||||
} else if (a < min_allowed) {
|
||||
a = min_allowed;
|
||||
}
|
||||
|
||||
opt_lg_san_uaf_align = a;
|
||||
CONF_CONTINUE;
|
||||
}
|
||||
|
||||
CONF_HANDLE_SIZE_T(opt_san_guard_small,
|
||||
"san_guard_small", 0, SIZE_T_MAX,
|
||||
@@ -1760,6 +1785,7 @@ malloc_init_hard_a0_locked() {
|
||||
prof_boot0();
|
||||
}
|
||||
malloc_conf_init(&sc_data, bin_shard_sizes);
|
||||
san_init(opt_lg_san_uaf_align);
|
||||
sz_boot(&sc_data, opt_cache_oblivious);
|
||||
bin_info_boot(&sc_data, bin_shard_sizes);
|
||||
|
||||
@@ -2970,6 +2996,41 @@ free_default(void *ptr) {
|
||||
}
|
||||
}
|
||||
|
||||
JEMALLOC_ALWAYS_INLINE bool
|
||||
free_fastpath_nonfast_aligned(void *ptr, bool check_prof) {
|
||||
/*
|
||||
* free_fastpath do not handle two uncommon cases: 1) sampled profiled
|
||||
* objects and 2) sampled junk & stash for use-after-free detection.
|
||||
* Both have special alignments which are used to escape the fastpath.
|
||||
*
|
||||
* prof_sample is page-aligned, which covers the UAF check when both
|
||||
* are enabled (the assertion below). Avoiding redundant checks since
|
||||
* this is on the fastpath -- at most one runtime branch from this.
|
||||
*/
|
||||
if (config_debug && cache_bin_nonfast_aligned(ptr)) {
|
||||
assert(prof_sample_aligned(ptr));
|
||||
}
|
||||
|
||||
if (config_prof && check_prof) {
|
||||
/* When prof is enabled, the prof_sample alignment is enough. */
|
||||
if (prof_sample_aligned(ptr)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config_uaf_detection) {
|
||||
if (cache_bin_nonfast_aligned(ptr)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Returns whether or not the free attempt was successful. */
|
||||
JEMALLOC_ALWAYS_INLINE
|
||||
bool free_fastpath(void *ptr, size_t size, bool size_hint) {
|
||||
@@ -2992,18 +3053,21 @@ bool free_fastpath(void *ptr, size_t size, bool size_hint) {
|
||||
&arena_emap_global, ptr, &alloc_ctx);
|
||||
|
||||
/* Note: profiled objects will have alloc_ctx.slab set */
|
||||
if (unlikely(err || !alloc_ctx.slab)) {
|
||||
if (unlikely(err || !alloc_ctx.slab ||
|
||||
free_fastpath_nonfast_aligned(ptr,
|
||||
/* check_prof */ false))) {
|
||||
return false;
|
||||
}
|
||||
assert(alloc_ctx.szind != SC_NSIZES);
|
||||
} else {
|
||||
/*
|
||||
* Check for both sizes that are too large, and for sampled
|
||||
* objects. Sampled objects are always page-aligned. The
|
||||
* sampled object check will also check for null ptr.
|
||||
* Check for both sizes that are too large, and for sampled /
|
||||
* special aligned objects. The alignment check will also check
|
||||
* for null ptr.
|
||||
*/
|
||||
if (unlikely(size > SC_LOOKUP_MAXCLASS ||
|
||||
(config_prof && prof_sample_aligned(ptr)))) {
|
||||
free_fastpath_nonfast_aligned(ptr,
|
||||
/* check_prof */ true))) {
|
||||
return false;
|
||||
}
|
||||
alloc_ctx.szind = sz_size2index_lookup(size);
|
||||
|
60
src/san.c
60
src/san.c
@@ -10,6 +10,15 @@
|
||||
size_t opt_san_guard_large = SAN_GUARD_LARGE_EVERY_N_EXTENTS_DEFAULT;
|
||||
size_t opt_san_guard_small = SAN_GUARD_SMALL_EVERY_N_EXTENTS_DEFAULT;
|
||||
|
||||
/* Aligned (-1 is off) ptrs will be junked & stashed on dealloc. */
|
||||
ssize_t opt_lg_san_uaf_align = SAN_LG_UAF_ALIGN_DEFAULT;
|
||||
|
||||
/*
|
||||
* Initialized in san_init(). When disabled, the mask is set to (uintptr_t)-1
|
||||
* to always fail the nonfast_align check.
|
||||
*/
|
||||
uintptr_t san_cache_bin_nonfast_mask = SAN_CACHE_BIN_NONFAST_MASK_DEFAULT;
|
||||
|
||||
static inline void
|
||||
san_find_guarded_addr(edata_t *edata, uintptr_t *guard1, uintptr_t *guard2,
|
||||
uintptr_t *addr, size_t size, bool left, bool right) {
|
||||
@@ -141,8 +150,59 @@ san_unguard_pages_pre_destroy(tsdn_t *tsdn, ehooks_t *ehooks, edata_t *edata,
|
||||
/* right */ true, /* remap */ false);
|
||||
}
|
||||
|
||||
static bool
|
||||
san_stashed_corrupted(void *ptr, size_t size) {
|
||||
if (san_junk_ptr_should_slow()) {
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (((char *)ptr)[i] != (char)uaf_detect_junk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void *first, *mid, *last;
|
||||
san_junk_ptr_locations(ptr, size, &first, &mid, &last);
|
||||
if (*(uintptr_t *)first != uaf_detect_junk ||
|
||||
*(uintptr_t *)mid != uaf_detect_junk ||
|
||||
*(uintptr_t *)last != uaf_detect_junk) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
san_check_stashed_ptrs(void **ptrs, size_t nstashed, size_t usize) {
|
||||
/*
|
||||
* Verify that the junked-filled & stashed pointers remain unchanged, to
|
||||
* detect write-after-free.
|
||||
*/
|
||||
for (size_t n = 0; n < nstashed; n++) {
|
||||
void *stashed = ptrs[n];
|
||||
assert(stashed != NULL);
|
||||
assert(cache_bin_nonfast_aligned(stashed));
|
||||
if (unlikely(san_stashed_corrupted(stashed, usize))) {
|
||||
safety_check_fail("<jemalloc>: Write-after-free "
|
||||
"detected on deallocated pointer %p (size %zu).\n",
|
||||
stashed, usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
tsd_san_init(tsd_t *tsd) {
|
||||
*tsd_san_extents_until_guard_smallp_get(tsd) = opt_san_guard_small;
|
||||
*tsd_san_extents_until_guard_largep_get(tsd) = opt_san_guard_large;
|
||||
}
|
||||
|
||||
void
|
||||
san_init(ssize_t lg_san_uaf_align) {
|
||||
assert(lg_san_uaf_align == -1 || lg_san_uaf_align >= LG_PAGE);
|
||||
if (lg_san_uaf_align == -1) {
|
||||
san_cache_bin_nonfast_mask = (uintptr_t)-1;
|
||||
return;
|
||||
}
|
||||
|
||||
san_cache_bin_nonfast_mask = ((uintptr_t)1 << lg_san_uaf_align) - 1;
|
||||
}
|
||||
|
47
src/tcache.c
47
src/tcache.c
@@ -4,6 +4,7 @@
|
||||
#include "jemalloc/internal/assert.h"
|
||||
#include "jemalloc/internal/mutex.h"
|
||||
#include "jemalloc/internal/safety_check.h"
|
||||
#include "jemalloc/internal/san.h"
|
||||
#include "jemalloc/internal/sc.h"
|
||||
|
||||
/******************************************************************************/
|
||||
@@ -179,6 +180,8 @@ tcache_event(tsd_t *tsd) {
|
||||
bool is_small = (szind < SC_NBINS);
|
||||
cache_bin_t *cache_bin = &tcache->bins[szind];
|
||||
|
||||
tcache_bin_flush_stashed(tsd, tcache, cache_bin, szind, is_small);
|
||||
|
||||
cache_bin_sz_t low_water = cache_bin_low_water_get(cache_bin,
|
||||
&tcache_bin_info[szind]);
|
||||
if (low_water > 0) {
|
||||
@@ -497,6 +500,8 @@ tcache_bin_flush_impl(tsd_t *tsd, tcache_t *tcache, cache_bin_t *cache_bin,
|
||||
JEMALLOC_ALWAYS_INLINE void
|
||||
tcache_bin_flush_bottom(tsd_t *tsd, tcache_t *tcache, cache_bin_t *cache_bin,
|
||||
szind_t binind, unsigned rem, bool small) {
|
||||
tcache_bin_flush_stashed(tsd, tcache, cache_bin, binind, small);
|
||||
|
||||
cache_bin_sz_t ncached = cache_bin_ncached_get_local(cache_bin,
|
||||
&tcache_bin_info[binind]);
|
||||
assert((cache_bin_sz_t)rem <= ncached);
|
||||
@@ -525,6 +530,48 @@ tcache_bin_flush_large(tsd_t *tsd, tcache_t *tcache, cache_bin_t *cache_bin,
|
||||
tcache_bin_flush_bottom(tsd, tcache, cache_bin, binind, rem, false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Flushing stashed happens when 1) tcache fill, 2) tcache flush, or 3) tcache
|
||||
* GC event. This makes sure that the stashed items do not hold memory for too
|
||||
* long, and new buffers can only be allocated when nothing is stashed.
|
||||
*
|
||||
* The downside is, the time between stash and flush may be relatively short,
|
||||
* especially when the request rate is high. It lowers the chance of detecting
|
||||
* write-after-free -- however that is a delayed detection anyway, and is less
|
||||
* of a focus than the memory overhead.
|
||||
*/
|
||||
void
|
||||
tcache_bin_flush_stashed(tsd_t *tsd, tcache_t *tcache, cache_bin_t *cache_bin,
|
||||
szind_t binind, bool is_small) {
|
||||
cache_bin_info_t *info = &tcache_bin_info[binind];
|
||||
/*
|
||||
* The two below are for assertion only. The content of original cached
|
||||
* items remain unchanged -- the stashed items reside on the other end
|
||||
* of the stack. Checking the stack head and ncached to verify.
|
||||
*/
|
||||
void *head_content = *cache_bin->stack_head;
|
||||
cache_bin_sz_t orig_cached = cache_bin_ncached_get_local(cache_bin,
|
||||
info);
|
||||
|
||||
cache_bin_sz_t nstashed = cache_bin_nstashed_get(cache_bin, info);
|
||||
assert(orig_cached + nstashed <= cache_bin_info_ncached_max(info));
|
||||
if (nstashed == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CACHE_BIN_PTR_ARRAY_DECLARE(ptrs, nstashed);
|
||||
cache_bin_init_ptr_array_for_stashed(cache_bin, binind, info, &ptrs,
|
||||
nstashed);
|
||||
san_check_stashed_ptrs(ptrs.ptr, nstashed, sz_index2size(binind));
|
||||
tcache_bin_flush_impl(tsd, tcache, cache_bin, binind, &ptrs, nstashed,
|
||||
is_small);
|
||||
cache_bin_finish_flush_stashed(cache_bin, info);
|
||||
|
||||
assert(cache_bin_nstashed_get(cache_bin, info) == 0);
|
||||
assert(cache_bin_ncached_get_local(cache_bin, info) == orig_cached);
|
||||
assert(head_content == *cache_bin->stack_head);
|
||||
}
|
||||
|
||||
void
|
||||
tcache_arena_associate(tsdn_t *tsdn, tcache_slow_t *tcache_slow,
|
||||
tcache_t *tcache, arena_t *arena) {
|
||||
|
Reference in New Issue
Block a user