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:
@@ -82,27 +82,30 @@ do_batch_alloc_test(cache_bin_t *bin, cache_bin_info_t *info, void **ptrs,
|
||||
free(out);
|
||||
}
|
||||
|
||||
static void
|
||||
test_bin_init(cache_bin_t *bin, cache_bin_info_t *info) {
|
||||
size_t size;
|
||||
size_t alignment;
|
||||
cache_bin_info_compute_alloc(info, 1, &size, &alignment);
|
||||
void *mem = mallocx(size, MALLOCX_ALIGN(alignment));
|
||||
assert_ptr_not_null(mem, "Unexpected mallocx failure");
|
||||
|
||||
size_t cur_offset = 0;
|
||||
cache_bin_preincrement(info, 1, mem, &cur_offset);
|
||||
cache_bin_init(bin, info, mem, &cur_offset);
|
||||
cache_bin_postincrement(info, 1, mem, &cur_offset);
|
||||
assert_zu_eq(cur_offset, size, "Should use all requested memory");
|
||||
}
|
||||
|
||||
TEST_BEGIN(test_cache_bin) {
|
||||
const int ncached_max = 100;
|
||||
bool success;
|
||||
void *ptr;
|
||||
|
||||
cache_bin_t bin;
|
||||
cache_bin_info_t info;
|
||||
cache_bin_info_init(&info, ncached_max);
|
||||
|
||||
size_t size;
|
||||
size_t alignment;
|
||||
cache_bin_info_compute_alloc(&info, 1, &size, &alignment);
|
||||
void *mem = mallocx(size, MALLOCX_ALIGN(alignment));
|
||||
assert_ptr_not_null(mem, "Unexpected mallocx failure");
|
||||
|
||||
size_t cur_offset = 0;
|
||||
cache_bin_preincrement(&info, 1, mem, &cur_offset);
|
||||
cache_bin_init(&bin, &info, mem, &cur_offset);
|
||||
cache_bin_postincrement(&info, 1, mem, &cur_offset);
|
||||
|
||||
assert_zu_eq(cur_offset, size, "Should use all requested memory");
|
||||
cache_bin_t bin;
|
||||
test_bin_init(&bin, &info);
|
||||
|
||||
/* Initialize to empty; should then have 0 elements. */
|
||||
expect_d_eq(ncached_max, cache_bin_info_ncached_max(&info), "");
|
||||
@@ -258,7 +261,123 @@ TEST_BEGIN(test_cache_bin) {
|
||||
}
|
||||
TEST_END
|
||||
|
||||
static void
|
||||
do_flush_stashed_test(cache_bin_t *bin, cache_bin_info_t *info, void **ptrs,
|
||||
cache_bin_sz_t nfill, cache_bin_sz_t nstash) {
|
||||
expect_true(cache_bin_ncached_get_local(bin, info) == 0,
|
||||
"Bin not empty");
|
||||
expect_true(cache_bin_nstashed_get(bin, info) == 0, "Bin not empty");
|
||||
expect_true(nfill + nstash <= info->ncached_max, "Exceeded max");
|
||||
|
||||
bool ret;
|
||||
/* Fill */
|
||||
for (cache_bin_sz_t i = 0; i < nfill; i++) {
|
||||
ret = cache_bin_dalloc_easy(bin, &ptrs[i]);
|
||||
expect_true(ret, "Unexpected fill failure");
|
||||
}
|
||||
expect_true(cache_bin_ncached_get_local(bin, info) == nfill,
|
||||
"Wrong cached count");
|
||||
|
||||
/* Stash */
|
||||
for (cache_bin_sz_t i = 0; i < nstash; i++) {
|
||||
ret = cache_bin_stash(bin, &ptrs[i + nfill]);
|
||||
expect_true(ret, "Unexpected stash failure");
|
||||
}
|
||||
expect_true(cache_bin_nstashed_get(bin, info) == nstash,
|
||||
"Wrong stashed count");
|
||||
|
||||
if (nfill + nstash == info->ncached_max) {
|
||||
ret = cache_bin_dalloc_easy(bin, &ptrs[0]);
|
||||
expect_false(ret, "Should not dalloc into a full bin");
|
||||
ret = cache_bin_stash(bin, &ptrs[0]);
|
||||
expect_false(ret, "Should not stash into a full bin");
|
||||
}
|
||||
|
||||
/* Alloc filled ones */
|
||||
for (cache_bin_sz_t i = 0; i < nfill; i++) {
|
||||
void *ptr = cache_bin_alloc(bin, &ret);
|
||||
expect_true(ret, "Unexpected alloc failure");
|
||||
/* Verify it's not from the stashed range. */
|
||||
expect_true((uintptr_t)ptr < (uintptr_t)&ptrs[nfill],
|
||||
"Should not alloc stashed ptrs");
|
||||
}
|
||||
expect_true(cache_bin_ncached_get_local(bin, info) == 0,
|
||||
"Wrong cached count");
|
||||
expect_true(cache_bin_nstashed_get(bin, info) == nstash,
|
||||
"Wrong stashed count");
|
||||
|
||||
cache_bin_alloc(bin, &ret);
|
||||
expect_false(ret, "Should not alloc stashed");
|
||||
|
||||
/* Clear stashed ones */
|
||||
cache_bin_finish_flush_stashed(bin, info);
|
||||
expect_true(cache_bin_ncached_get_local(bin, info) == 0,
|
||||
"Wrong cached count");
|
||||
expect_true(cache_bin_nstashed_get(bin, info) == 0,
|
||||
"Wrong stashed count");
|
||||
|
||||
cache_bin_alloc(bin, &ret);
|
||||
expect_false(ret, "Should not alloc from empty bin");
|
||||
}
|
||||
|
||||
TEST_BEGIN(test_cache_bin_stash) {
|
||||
const int ncached_max = 100;
|
||||
|
||||
cache_bin_t bin;
|
||||
cache_bin_info_t info;
|
||||
cache_bin_info_init(&info, ncached_max);
|
||||
test_bin_init(&bin, &info);
|
||||
|
||||
/*
|
||||
* The content of this array is not accessed; instead the interior
|
||||
* addresses are used to insert / stash into the bins as test pointers.
|
||||
*/
|
||||
void **ptrs = mallocx(sizeof(void *) * (ncached_max + 1), 0);
|
||||
assert_ptr_not_null(ptrs, "Unexpected mallocx failure");
|
||||
bool ret;
|
||||
for (cache_bin_sz_t i = 0; i < ncached_max; i++) {
|
||||
expect_true(cache_bin_ncached_get_local(&bin, &info) ==
|
||||
(i / 2 + i % 2), "Wrong ncached value");
|
||||
expect_true(cache_bin_nstashed_get(&bin, &info) == i / 2,
|
||||
"Wrong nstashed value");
|
||||
if (i % 2 == 0) {
|
||||
cache_bin_dalloc_easy(&bin, &ptrs[i]);
|
||||
} else {
|
||||
ret = cache_bin_stash(&bin, &ptrs[i]);
|
||||
expect_true(ret, "Should be able to stash into a "
|
||||
"non-full cache bin");
|
||||
}
|
||||
}
|
||||
ret = cache_bin_dalloc_easy(&bin, &ptrs[0]);
|
||||
expect_false(ret, "Should not dalloc into a full cache bin");
|
||||
ret = cache_bin_stash(&bin, &ptrs[0]);
|
||||
expect_false(ret, "Should not stash into a full cache bin");
|
||||
for (cache_bin_sz_t i = 0; i < ncached_max; i++) {
|
||||
void *ptr = cache_bin_alloc(&bin, &ret);
|
||||
if (i < ncached_max / 2) {
|
||||
expect_true(ret, "Should be able to alloc");
|
||||
uintptr_t diff = ((uintptr_t)ptr - (uintptr_t)&ptrs[0])
|
||||
/ sizeof(void *);
|
||||
expect_true(diff % 2 == 0, "Should be able to alloc");
|
||||
} else {
|
||||
expect_false(ret, "Should not alloc stashed");
|
||||
expect_true(cache_bin_nstashed_get(&bin, &info) ==
|
||||
ncached_max / 2, "Wrong nstashed value");
|
||||
}
|
||||
}
|
||||
|
||||
test_bin_init(&bin, &info);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, ncached_max, 0);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, 0, ncached_max);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, ncached_max / 2, ncached_max / 2);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, ncached_max / 4, ncached_max / 2);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, ncached_max / 2, ncached_max / 4);
|
||||
do_flush_stashed_test(&bin, &info, ptrs, ncached_max / 4, ncached_max / 4);
|
||||
}
|
||||
TEST_END
|
||||
|
||||
int
|
||||
main(void) {
|
||||
return test(test_cache_bin);
|
||||
return test(test_cache_bin,
|
||||
test_cache_bin_stash);
|
||||
}
|
||||
|
@@ -323,6 +323,7 @@ TEST_BEGIN(test_mallctl_opt) {
|
||||
TEST_MALLCTL_OPT(ssize_t, prof_recent_alloc_max, prof);
|
||||
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);
|
||||
|
||||
#undef TEST_MALLCTL_OPT
|
||||
}
|
||||
@@ -368,7 +369,7 @@ TEST_BEGIN(test_tcache_none) {
|
||||
/* Make sure that tcache-based allocation returns p, not q. */
|
||||
void *p1 = mallocx(42, 0);
|
||||
expect_ptr_not_null(p1, "Unexpected mallocx() failure");
|
||||
if (!opt_prof) {
|
||||
if (!opt_prof && !san_uaf_detection_enabled()) {
|
||||
expect_ptr_eq(p0, p1,
|
||||
"Expected tcache to allocate cached region");
|
||||
}
|
||||
@@ -434,8 +435,10 @@ TEST_BEGIN(test_tcache) {
|
||||
ps[i] = mallocx(psz, MALLOCX_TCACHE(tis[i]));
|
||||
expect_ptr_not_null(ps[i], "Unexpected mallocx() failure, i=%u",
|
||||
i);
|
||||
expect_ptr_eq(ps[i], p0,
|
||||
"Expected mallocx() to allocate cached region, i=%u", i);
|
||||
if (!san_uaf_detection_enabled()) {
|
||||
expect_ptr_eq(ps[i], p0, "Expected mallocx() to "
|
||||
"allocate cached region, i=%u", i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verify that reallocation uses cached regions. */
|
||||
@@ -444,8 +447,10 @@ TEST_BEGIN(test_tcache) {
|
||||
qs[i] = rallocx(ps[i], qsz, MALLOCX_TCACHE(tis[i]));
|
||||
expect_ptr_not_null(qs[i], "Unexpected rallocx() failure, i=%u",
|
||||
i);
|
||||
expect_ptr_eq(qs[i], q0,
|
||||
"Expected rallocx() to allocate cached region, i=%u", i);
|
||||
if (!san_uaf_detection_enabled()) {
|
||||
expect_ptr_eq(qs[i], q0, "Expected rallocx() to "
|
||||
"allocate cached region, i=%u", i);
|
||||
}
|
||||
/* Avoid undefined behavior in case of test failure. */
|
||||
if (qs[i] == NULL) {
|
||||
qs[i] = ps[i];
|
||||
|
@@ -152,6 +152,7 @@ TEST_BEGIN(test_tcache_max) {
|
||||
test_skip_if(!config_stats);
|
||||
test_skip_if(!opt_tcache);
|
||||
test_skip_if(opt_prof);
|
||||
test_skip_if(san_uaf_detection_enabled());
|
||||
|
||||
for (alloc_option = alloc_option_start;
|
||||
alloc_option < alloc_option_end;
|
||||
|
@@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
export MALLOC_CONF="tcache_max:1024"
|
||||
export MALLOC_CONF="tcache_max:1024,lg_san_uaf_align:-1"
|
||||
|
225
test/unit/uaf.c
Normal file
225
test/unit/uaf.c
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "test/jemalloc_test.h"
|
||||
#include "test/arena_util.h"
|
||||
|
||||
#include "jemalloc/internal/cache_bin.h"
|
||||
#include "jemalloc/internal/safety_check.h"
|
||||
|
||||
static size_t san_uaf_align;
|
||||
|
||||
static bool fake_abort_called;
|
||||
void fake_abort(const char *message) {
|
||||
(void)message;
|
||||
fake_abort_called = true;
|
||||
}
|
||||
|
||||
static void
|
||||
test_write_after_free_pre(void) {
|
||||
safety_check_set_abort(&fake_abort);
|
||||
fake_abort_called = false;
|
||||
}
|
||||
|
||||
static void
|
||||
test_write_after_free_post(void) {
|
||||
assert_d_eq(mallctl("thread.tcache.flush", NULL, NULL, NULL, 0),
|
||||
0, "Unexpected tcache flush failure");
|
||||
expect_true(fake_abort_called, "Use-after-free check didn't fire.");
|
||||
safety_check_set_abort(NULL);
|
||||
}
|
||||
|
||||
static bool
|
||||
uaf_detection_enabled(void) {
|
||||
if (!config_uaf_detection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t lg_san_uaf_align;
|
||||
size_t sz = sizeof(lg_san_uaf_align);
|
||||
assert_d_eq(mallctl("opt.lg_san_uaf_align", &lg_san_uaf_align, &sz,
|
||||
NULL, 0), 0, "Unexpected mallctl failure");
|
||||
if (lg_san_uaf_align < 0) {
|
||||
return false;
|
||||
}
|
||||
assert_zd_ge(lg_san_uaf_align, LG_PAGE, "san_uaf_align out of range");
|
||||
san_uaf_align = (size_t)1 << lg_san_uaf_align;
|
||||
|
||||
bool tcache_enabled;
|
||||
sz = sizeof(tcache_enabled);
|
||||
assert_d_eq(mallctl("thread.tcache.enabled", &tcache_enabled, &sz, NULL,
|
||||
0), 0, "Unexpected mallctl failure");
|
||||
if (!tcache_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
test_use_after_free(size_t alloc_size, bool write_after_free) {
|
||||
void *ptr = (void *)(uintptr_t)san_uaf_align;
|
||||
assert_true(cache_bin_nonfast_aligned(ptr), "Wrong alignment");
|
||||
ptr = (void *)((uintptr_t)123 * (uintptr_t)san_uaf_align);
|
||||
assert_true(cache_bin_nonfast_aligned(ptr), "Wrong alignment");
|
||||
ptr = (void *)((uintptr_t)san_uaf_align + 1);
|
||||
assert_false(cache_bin_nonfast_aligned(ptr), "Wrong alignment");
|
||||
|
||||
/*
|
||||
* Disable purging (-1) so that all dirty pages remain committed, to
|
||||
* make use-after-free tolerable.
|
||||
*/
|
||||
unsigned arena_ind = do_arena_create(-1, -1);
|
||||
int flags = MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE;
|
||||
|
||||
size_t n_max = san_uaf_align * 2;
|
||||
void **items = mallocx(n_max * sizeof(void *), flags);
|
||||
assert_ptr_not_null(items, "Unexpected mallocx failure");
|
||||
|
||||
bool found = false;
|
||||
size_t iter = 0;
|
||||
char magic = 's';
|
||||
assert_d_eq(mallctl("thread.tcache.flush", NULL, NULL, NULL, 0),
|
||||
0, "Unexpected tcache flush failure");
|
||||
while (!found) {
|
||||
ptr = mallocx(alloc_size, flags);
|
||||
assert_ptr_not_null(ptr, "Unexpected mallocx failure");
|
||||
|
||||
found = cache_bin_nonfast_aligned(ptr);
|
||||
*(char *)ptr = magic;
|
||||
items[iter] = ptr;
|
||||
assert_zu_lt(iter++, n_max, "No aligned ptr found");
|
||||
}
|
||||
|
||||
if (write_after_free) {
|
||||
test_write_after_free_pre();
|
||||
}
|
||||
bool junked = false;
|
||||
while (iter-- != 0) {
|
||||
char *volatile mem = items[iter];
|
||||
assert_c_eq(*mem, magic, "Unexpected memory content");
|
||||
free(mem);
|
||||
if (*mem != magic) {
|
||||
junked = true;
|
||||
assert_c_eq(*mem, (char)uaf_detect_junk,
|
||||
"Unexpected junk-filling bytes");
|
||||
if (write_after_free) {
|
||||
*(char *)mem = magic + 1;
|
||||
}
|
||||
}
|
||||
/* Flush tcache (including stashed). */
|
||||
assert_d_eq(mallctl("thread.tcache.flush", NULL, NULL, NULL, 0),
|
||||
0, "Unexpected tcache flush failure");
|
||||
}
|
||||
expect_true(junked, "Aligned ptr not junked");
|
||||
if (write_after_free) {
|
||||
test_write_after_free_post();
|
||||
}
|
||||
|
||||
dallocx(items, flags);
|
||||
do_arena_destroy(arena_ind);
|
||||
}
|
||||
|
||||
TEST_BEGIN(test_read_after_free) {
|
||||
test_skip_if(!uaf_detection_enabled());
|
||||
|
||||
test_use_after_free(sizeof(void *), /* write_after_free */ false);
|
||||
test_use_after_free(sizeof(void *) + 1, /* write_after_free */ false);
|
||||
test_use_after_free(16, /* write_after_free */ false);
|
||||
test_use_after_free(20, /* write_after_free */ false);
|
||||
test_use_after_free(32, /* write_after_free */ false);
|
||||
test_use_after_free(33, /* write_after_free */ false);
|
||||
test_use_after_free(48, /* write_after_free */ false);
|
||||
test_use_after_free(64, /* write_after_free */ false);
|
||||
test_use_after_free(65, /* write_after_free */ false);
|
||||
test_use_after_free(129, /* write_after_free */ false);
|
||||
test_use_after_free(255, /* write_after_free */ false);
|
||||
test_use_after_free(256, /* write_after_free */ false);
|
||||
}
|
||||
TEST_END
|
||||
|
||||
TEST_BEGIN(test_write_after_free) {
|
||||
test_skip_if(!uaf_detection_enabled());
|
||||
|
||||
test_use_after_free(sizeof(void *), /* write_after_free */ true);
|
||||
test_use_after_free(sizeof(void *) + 1, /* write_after_free */ true);
|
||||
test_use_after_free(16, /* write_after_free */ true);
|
||||
test_use_after_free(20, /* write_after_free */ true);
|
||||
test_use_after_free(32, /* write_after_free */ true);
|
||||
test_use_after_free(33, /* write_after_free */ true);
|
||||
test_use_after_free(48, /* write_after_free */ true);
|
||||
test_use_after_free(64, /* write_after_free */ true);
|
||||
test_use_after_free(65, /* write_after_free */ true);
|
||||
test_use_after_free(129, /* write_after_free */ true);
|
||||
test_use_after_free(255, /* write_after_free */ true);
|
||||
test_use_after_free(256, /* write_after_free */ true);
|
||||
}
|
||||
TEST_END
|
||||
|
||||
static bool
|
||||
check_allocated_intact(void **allocated, size_t n_alloc) {
|
||||
for (unsigned i = 0; i < n_alloc; i++) {
|
||||
void *ptr = *(void **)allocated[i];
|
||||
bool found = false;
|
||||
for (unsigned j = 0; j < n_alloc; j++) {
|
||||
if (ptr == allocated[j]) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_BEGIN(test_use_after_free_integration) {
|
||||
test_skip_if(!uaf_detection_enabled());
|
||||
|
||||
unsigned arena_ind = do_arena_create(-1, -1);
|
||||
int flags = MALLOCX_ARENA(arena_ind);
|
||||
|
||||
size_t n_alloc = san_uaf_align * 2;
|
||||
void **allocated = mallocx(n_alloc * sizeof(void *), flags);
|
||||
assert_ptr_not_null(allocated, "Unexpected mallocx failure");
|
||||
|
||||
for (unsigned i = 0; i < n_alloc; i++) {
|
||||
allocated[i] = mallocx(sizeof(void *) * 8, flags);
|
||||
assert_ptr_not_null(allocated[i], "Unexpected mallocx failure");
|
||||
if (i > 0) {
|
||||
/* Emulate a circular list. */
|
||||
*(void **)allocated[i] = allocated[i - 1];
|
||||
}
|
||||
}
|
||||
*(void **)allocated[0] = allocated[n_alloc - 1];
|
||||
expect_true(check_allocated_intact(allocated, n_alloc),
|
||||
"Allocated data corrupted");
|
||||
|
||||
for (unsigned i = 0; i < n_alloc; i++) {
|
||||
free(allocated[i]);
|
||||
}
|
||||
/* Read-after-free */
|
||||
expect_false(check_allocated_intact(allocated, n_alloc),
|
||||
"Junk-filling not detected");
|
||||
|
||||
test_write_after_free_pre();
|
||||
for (unsigned i = 0; i < n_alloc; i++) {
|
||||
allocated[i] = mallocx(sizeof(void *), flags);
|
||||
assert_ptr_not_null(allocated[i], "Unexpected mallocx failure");
|
||||
*(void **)allocated[i] = (void *)(uintptr_t)i;
|
||||
}
|
||||
/* Write-after-free */
|
||||
for (unsigned i = 0; i < n_alloc; i++) {
|
||||
free(allocated[i]);
|
||||
*(void **)allocated[i] = NULL;
|
||||
}
|
||||
test_write_after_free_post();
|
||||
}
|
||||
TEST_END
|
||||
|
||||
int
|
||||
main(void) {
|
||||
return test(
|
||||
test_read_after_free,
|
||||
test_write_after_free,
|
||||
test_use_after_free_integration);
|
||||
}
|
3
test/unit/uaf.sh
Normal file
3
test/unit/uaf.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
export MALLOC_CONF="lg_san_uaf_align:12"
|
Reference in New Issue
Block a user