Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ typedef int pid_t;
#include <errno.h>
#ifdef __SANITIZE_ADDRESS__
# include <sanitizer/asan_interface.h>
#else
# define ASAN_POISON_MEMORY_REGION(addr, size) ((void) 0)
# define ASAN_UNPOISON_MEMORY_REGION(addr, size) ((void) 0)
#endif

#ifndef _WIN32
Expand Down Expand Up @@ -890,9 +893,6 @@ static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment)
if (zend_mm_use_huge_pages) {
zend_mm_hugepage(ptr, size);
}
#ifdef __SANITIZE_ADDRESS__
ASAN_UNPOISON_MEMORY_REGION(ptr, size);
#endif
return ptr;
} else {
size_t offset;
Expand Down Expand Up @@ -932,34 +932,40 @@ static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment)
if (zend_mm_use_huge_pages) {
zend_mm_hugepage(ptr, size);
}
# ifdef __SANITIZE_ADDRESS__
ASAN_UNPOISON_MEMORY_REGION(ptr, size);
# endif
#endif
return ptr;
}
}

static void *zend_mm_chunk_alloc(zend_mm_heap *heap, size_t size, size_t alignment)
{
void *ptr;

#if ZEND_MM_STORAGE
if (UNEXPECTED(heap->storage)) {
void *ptr = heap->storage->handlers.chunk_alloc(heap->storage, size, alignment);
ptr = heap->storage->handlers.chunk_alloc(heap->storage, size, alignment);
ZEND_ASSERT(((uintptr_t)((char*)ptr + (alignment-1)) & (alignment-1)) == (uintptr_t)ptr);
return ptr;
}
} else
#endif
return zend_mm_chunk_alloc_int(size, alignment);
{
ptr = zend_mm_chunk_alloc_int(size, alignment);
}
if (ptr != NULL) {
ASAN_UNPOISON_MEMORY_REGION(ptr, size);
}
return ptr;
}

static void zend_mm_chunk_free(zend_mm_heap *heap, void *addr, size_t size)
{
#if ZEND_MM_STORAGE
if (UNEXPECTED(heap->storage)) {
ASAN_UNPOISON_MEMORY_REGION(addr, size);
heap->storage->handlers.chunk_free(heap->storage, addr, size);
return;
}
#endif
ASAN_POISON_MEMORY_REGION(addr, size);
zend_mm_munmap(addr, size);
}

Expand All @@ -975,6 +981,7 @@ static int zend_mm_chunk_truncate(zend_mm_heap *heap, void *addr, size_t old_siz
}
#endif
#ifndef _WIN32
ASAN_POISON_MEMORY_REGION((char*)addr + new_size, old_size - new_size);
zend_mm_munmap((char*)addr + new_size, old_size - new_size);
return 1;
#else
Expand Down Expand Up @@ -1300,6 +1307,7 @@ static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count ZEND_F
if (page_num == chunk->free_tail) {
chunk->free_tail = page_num + pages_count;
}
ASAN_UNPOISON_MEMORY_REGION(ZEND_MM_PAGE_ADDR(chunk, page_num), pages_count * ZEND_MM_PAGE_SIZE);
return ZEND_MM_PAGE_ADDR(chunk, page_num);
}

Expand Down Expand Up @@ -1370,6 +1378,7 @@ static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_

static zend_always_inline void zend_mm_free_pages_ex(zend_mm_heap *heap, zend_mm_chunk *chunk, uint32_t page_num, uint32_t pages_count, int free_chunk)
{
ASAN_POISON_MEMORY_REGION(ZEND_MM_PAGE_ADDR(chunk, page_num), pages_count * ZEND_MM_PAGE_SIZE);
chunk->free_pages += pages_count;
zend_mm_bitset_reset_range(chunk->free_map, page_num, pages_count);
chunk->map[page_num] = 0;
Expand Down Expand Up @@ -1461,6 +1470,20 @@ static zend_always_inline uint32_t zend_mm_small_size_to_bin(size_t size)

#define ZEND_MM_SMALL_SIZE_TO_BIN(size) zend_mm_small_size_to_bin(size)

#ifdef __SANITIZE_ADDRESS__
static zend_always_inline void zend_mm_asan_poison_free_slot(void *ptr, uint32_t bin_num)
{
size_t size = bin_data_size[bin_num];
#if ZEND_DEBUG
/* Leak detection inspects the debug trailer of free slots. */
size -= ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info));
#endif
ASAN_POISON_MEMORY_REGION(ptr, size);
}
#else
# define zend_mm_asan_poison_free_slot(ptr, bin_num) ((void) 0)
#endif

#if ZEND_MM_HEAP_PROTECTION
/* We keep track of free slots by organizing them in a linked list, with the
* first word of every free slot being a pointer to the next one.
Expand Down Expand Up @@ -1507,29 +1530,43 @@ static zend_always_inline void zend_mm_set_next_free_slot(zend_mm_heap *heap, ui
{
ZEND_ASSERT(bin_data_size[bin_num] >= ZEND_MM_MIN_USEABLE_BIN_SIZE);

/* Free-list links live in poisoned storage, so expose them only while in use. */
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[bin_num]);
slot->next_free_slot = next;

zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, bin_num);
*shadow_addr = zend_mm_encode_free_slot(heap, shadow_addr, next);
zend_mm_asan_poison_free_slot(slot, bin_num);
}

static zend_always_inline zend_mm_free_slot *zend_mm_get_next_free_slot(zend_mm_heap *heap, uint32_t bin_num, zend_mm_free_slot* slot)
{
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[bin_num]);
zend_mm_free_slot *next = slot->next_free_slot;
if (EXPECTED(next != NULL)) {
zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, bin_num);
if (UNEXPECTED(next != zend_mm_decode_free_slot(heap, shadow_addr, *shadow_addr))) {
zend_mm_panic("zend_mm_heap corrupted");
}
}
zend_mm_asan_poison_free_slot(slot, bin_num);
return (zend_mm_free_slot*)next;
}

#else /* ZEND_MM_HEAP_PROTECTION */
# define zend_mm_set_next_free_slot(heap, bin_num, slot, next) do { \
(slot)->next_free_slot = (next); \
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[bin_num]); \
(slot)->next_free_slot = (next); \
zend_mm_asan_poison_free_slot(slot, bin_num); \
} while (0)
# define zend_mm_get_next_free_slot(heap, bin_num, slot) (slot)->next_free_slot

static zend_always_inline zend_mm_free_slot *zend_mm_get_next_free_slot(zend_mm_heap *heap, uint32_t bin_num, zend_mm_free_slot *slot)
{
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[bin_num]);
zend_mm_free_slot *next = slot->next_free_slot;
zend_mm_asan_poison_free_slot(slot, bin_num);
return next;
}
#endif /* ZEND_MM_HEAP_PROTECTION */

static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
Expand Down Expand Up @@ -1588,6 +1625,7 @@ static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint
dbg->size = 0;
} while (0);
#endif
zend_mm_asan_poison_free_slot(p, bin_num);

/* return first element */
return bin;
Expand All @@ -1609,6 +1647,7 @@ static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, uint32_t
if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
zend_mm_free_slot *p = heap->free_slot[bin_num];
heap->free_slot[bin_num] = zend_mm_get_next_free_slot(heap, bin_num, p);
ASAN_UNPOISON_MEMORY_REGION(p, bin_data_size[bin_num]);
return p;
} else {
return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
Expand Down Expand Up @@ -1861,6 +1900,7 @@ static zend_never_inline void *zend_mm_realloc_huge(zend_mm_heap *heap, void *pt

/* try to map tail right after this block */
if (zend_mm_chunk_extend(heap, ptr, old_size, new_size)) {
ASAN_UNPOISON_MEMORY_REGION((char*)ptr + old_size, new_size - old_size);
#if ZEND_MM_STAT || ZEND_MM_LIMIT
heap->real_size += new_size - old_size;
#endif
Expand Down Expand Up @@ -1997,6 +2037,7 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
int new_pages_count = (int)(new_size / ZEND_MM_PAGE_SIZE);
int rest_pages_count = (int)((old_size - new_size) / ZEND_MM_PAGE_SIZE);

ASAN_POISON_MEMORY_REGION((char*)ptr + new_size, old_size - new_size);
#if ZEND_MM_STAT
heap->size -= rest_pages_count * ZEND_MM_PAGE_SIZE;
#endif
Expand All @@ -2019,6 +2060,7 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
/* try to allocate tail pages after this block */
if (page_num + new_pages_count <= ZEND_MM_PAGES &&
zend_mm_bitset_is_free_range(chunk->free_map, page_num + old_pages_count, new_pages_count - old_pages_count)) {
ASAN_UNPOISON_MEMORY_REGION((char*)ptr + old_size, new_size - old_size);
#if ZEND_MM_STAT
do {
size_t size = heap->size + (new_size - old_size);
Expand Down Expand Up @@ -2266,14 +2308,17 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
continue;
}
zend_mm_free_slot *next;
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[i]);
while ((next = slot->next_free_slot)) {
zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, i);
if (UNEXPECTED(next != zend_mm_decode_free_slot_key(old_key, shadow_addr, *shadow_addr))) {
zend_mm_panic("zend_mm_heap corrupted");
}
zend_mm_set_next_free_slot(heap, i, slot, next);
slot = next;
ASAN_UNPOISON_MEMORY_REGION(slot, bin_data_size[i]);
}
zend_mm_asan_poison_free_slot(slot, i);
}

zend_mm_rekey_cached_chunks(heap, old_key);
Expand Down Expand Up @@ -2775,13 +2820,17 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
p = heap->cached_chunks;
while (p != NULL) {
zend_mm_chunk *q = zend_mm_get_next_cached_chunk(heap, p);
ASAN_POISON_MEMORY_REGION(ZEND_MM_PAGE_ADDR(p, ZEND_MM_FIRST_PAGE),
ZEND_MM_CHUNK_SIZE - ZEND_MM_FIRST_PAGE * ZEND_MM_PAGE_SIZE);
memset(p, 0, sizeof(zend_mm_chunk));
zend_mm_set_next_cached_chunk(heap, p, q);
p = q;
}

/* reinitialize the first chunk and heap */
p = heap->main_chunk;
ASAN_POISON_MEMORY_REGION(ZEND_MM_PAGE_ADDR(p, ZEND_MM_FIRST_PAGE),
ZEND_MM_CHUNK_SIZE - ZEND_MM_FIRST_PAGE * ZEND_MM_PAGE_SIZE);
p->heap = &p->heap_slot;
p->next = p;
p->prev = p;
Expand Down
22 changes: 22 additions & 0 deletions docs/source/miscellaneous/running-tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ The test runner can run tests in parallel, by using the ``-j`` option:

php run-tests.php -j24 ext/date/*.phpt

Testing the Native Allocator with AddressSanitizer
==================================================

Build with ``--enable-address-sanitizer`` to enable AddressSanitizer (ASan). The test runner's
``--asan`` option selects the system allocator with allocation tracking. To test native ZendMM
allocation behavior instead, omit ``--asan``, set ``USE_ZEND_ALLOC=1``, and retain the
sanitizer-specific test exclusions:

.. code:: shell

USE_ZEND_ALLOC=1 SKIP_ASAN=1 SKIP_PERF_SENSITIVE=1 \
sapi/cli/php run-tests.php Zend/tests

Native ZendMM poisons freed small allocations and page runs, and unpoisons them when reused. It also
poisons storage released during request shutdown. These annotations detect accesses to freed storage
until it is reused; they do not provide a quarantine or exact requested-size bounds checking.
Allocation size-class padding remains accessible, as do the debug trailers inspected by leak
detection in debug builds.

The ``zend_mm_asan.phpt`` test in ``ext/zend_test/tests`` checks allocator shadow state directly and
requires both ASan and ``--enable-zend-test``.

**************
Test results
**************
Expand Down
122 changes: 122 additions & 0 deletions ext/zend_test/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@

#include <signal.h>

#ifdef __SANITIZE_ADDRESS__
# include <sanitizer/asan_interface.h>
#endif

#if defined(HAVE_LIBXML) && !defined(PHP_WIN32)
# include <libxml/globals.h>
# include <libxml/parser.h>
Expand Down Expand Up @@ -269,6 +273,124 @@ static ZEND_FUNCTION(zend_test_callback)
free(p);
}

#ifdef __SANITIZE_ADDRESS__
static ZEND_FUNCTION(zend_test_asan_zend_mm)
{
zend_long old_size, new_size;

ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_LONG(old_size)
Z_PARAM_LONG(new_size)
ZEND_PARSE_PARAMETERS_END();

if (old_size < 1) {
zend_argument_value_error(1, "must be greater than 0");
RETURN_THROWS();
}
if (new_size < 1) {
zend_argument_value_error(2, "must be greater than 0");
RETURN_THROWS();
}

zend_mm_heap *heap = zend_mm_startup();
char *ptr = zend_mm_alloc(heap, old_size);
bool allocated = __asan_region_is_poisoned(ptr, old_size) == NULL;
memset(ptr, 0x42, old_size);

size_t old_capacity = zend_mm_block_size(heap, ptr);
/* Unmapped huge allocations need not retain ASan shadow state. */
bool old_huge = old_capacity > ZEND_MM_MAX_LARGE_SIZE;
char *old_ptr = ptr;
ptr = zend_mm_realloc(heap, ptr, new_size);
size_t new_capacity = zend_mm_block_size(heap, ptr);
bool new_huge = new_capacity > ZEND_MM_MAX_LARGE_SIZE;
bool reallocated = __asan_region_is_poisoned(ptr, new_size) == NULL;
bool moved = ptr == old_ptr || old_huge || __asan_address_is_poisoned(old_ptr);
bool truncated = ptr != old_ptr || old_huge || new_capacity >= old_capacity
|| __asan_address_is_poisoned(ptr + old_capacity - 1);
bool preserved = true;
for (size_t i = 0; i < (size_t) MIN(old_size, new_size); i++) {
preserved &= ptr[i] == 0x42;
}
zend_mm_free(heap, ptr);
bool released = new_huge || (__asan_address_is_poisoned(ptr)
&& __asan_address_is_poisoned(ptr + new_size - 1));
zend_mm_refresh_key_child(heap);
bool rekeyed = new_huge || __asan_address_is_poisoned(ptr);
ptr = zend_mm_alloc(heap, new_size);
bool reused = __asan_region_is_poisoned(ptr, new_size) == NULL;
zend_mm_free(heap, ptr);
zend_mm_gc(heap);
bool collected = new_huge || __asan_address_is_poisoned(ptr);

ptr = zend_mm_realloc2(heap, NULL, old_size, 0);
memset(ptr, 0x24, old_size);
ptr = zend_mm_realloc2(heap, ptr, new_size, MIN(old_size, new_size));
bool copied = __asan_region_is_poisoned(ptr, new_size) == NULL;
for (size_t i = 0; i < (size_t) MIN(old_size, new_size); i++) {
copied &= ptr[i] == 0x24;
}
zend_mm_free(heap, ptr);

ptr = zend_mm_alloc(heap, old_size);
reused &= __asan_region_is_poisoned(ptr, old_size) == NULL;
zend_mm_shutdown(heap, false, true);
bool reset = old_huge || (__asan_address_is_poisoned(ptr)
&& __asan_address_is_poisoned(ptr + old_size - 1));
ptr = zend_mm_alloc(heap, old_size);
bool restarted = __asan_region_is_poisoned(ptr, old_size) == NULL;
zend_mm_free(heap, ptr);

zend_mm_heap *orig_heap = zend_mm_set_heap(heap);
ptr = emalloc(1);
bool specialized = __asan_region_is_poisoned(ptr, 1) == NULL;
efree_size(ptr, 1);
specialized &= __asan_address_is_poisoned(ptr);
ptr = emalloc(64);
specialized &= __asan_region_is_poisoned(ptr, 64) == NULL;
efree_size(ptr, 64);
specialized &= __asan_address_is_poisoned(ptr);
ptr = emalloc(8192);
specialized &= __asan_region_is_poisoned(ptr, 8192) == NULL;
efree_size(ptr, 8192);
specialized &= __asan_address_is_poisoned(ptr);
zend_mm_set_heap(orig_heap);

char *blocks[4];
for (size_t i = 0; i < 4; i++) {
blocks[i] = zend_mm_alloc(heap, 1024 * 1024);
memset(blocks[i], 0x42, 1024 * 1024);
}
zend_mm_shutdown(heap, false, true);
bool cached = true;
for (size_t i = 0; i < 4; i++) {
blocks[i] = zend_mm_alloc(heap, 1024 * 1024);
cached &= __asan_region_is_poisoned(blocks[i], 1024 * 1024) == NULL;
memset(blocks[i], 0x24, 1024 * 1024);
}
for (size_t i = 0; i < 4; i++) {
zend_mm_free(heap, blocks[i]);
}
zend_mm_shutdown(heap, true, true);

array_init(return_value);
add_assoc_bool(return_value, "allocated", allocated);
add_assoc_bool(return_value, "reallocated", reallocated);
add_assoc_bool(return_value, "moved", moved);
add_assoc_bool(return_value, "truncated", truncated);
add_assoc_bool(return_value, "preserved", preserved);
add_assoc_bool(return_value, "released", released);
add_assoc_bool(return_value, "rekeyed", rekeyed);
add_assoc_bool(return_value, "collected", collected);
add_assoc_bool(return_value, "reused", reused);
add_assoc_bool(return_value, "copied", copied);
add_assoc_bool(return_value, "reset", reset);
add_assoc_bool(return_value, "restarted", restarted);
add_assoc_bool(return_value, "specialized", specialized);
add_assoc_bool(return_value, "cached", cached);
}
#endif

/* Leak a refcounted variable */
static ZEND_FUNCTION(zend_leak_variable)
{
Expand Down
Loading
Loading