mirror of
https://github.com/php/php-src.git
synced 2026-03-24 00:02:20 +01:00
Add runtime-enabled heap debugging capabilities (#18172)
Debugging memory corruption issues in production can be difficult when it's not possible to use a debug build or ASAN/MSAN/Valgrind (e.g. for performance reasons).
This change makes it possible to enable some basic heap debugging helpers without rebuilding PHP. This is controlled by the environment variable ZEND_MM_DEBUG. The env var takes a comma-separated list of parameters:
- poison_free=byte: Override freed blocks with the specified byte value (represented as a number)
- poison_alloc=byte: Override newly allocated blocks with the specified byte value (represented as a number)
- padding=bytes: Pad allocated blocks with the specified amount of bytes (if non-zero, a value >= 16 is recommended to not break alignments)
- check_freelists_on_shutdown=0|1: Enable checking freelist consistency [1] on shutdown
Example:
ZEND_MM_DEBUG=poison_free=0xbe,poison_alloc=0xeb,padding=16,check_freelists_on_shutdown=1 php ...
This is implemented by installing custom handlers when ZEND_MM_DEBUG is set.
This has zero overhead when ZEND_MM_DEBUG is not set. When ZEND_MM_DEBUG is set, the overhead is about 8.5% on the Symfony Demo benchmark.
Goals:
- Crash earlier after a memory corruption, to extract a useful backtrace
- Be usable in production with reasonable overhead
- Having zero overhead when not enabled
Non-goals:
- Replace debug builds, valgrind, ASAN, MSAN or other sanitizers
[1] https://github.com/php/php-src/pull/14054
Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>
This commit is contained in:
@@ -305,7 +305,17 @@ struct _zend_mm_heap {
|
||||
size_t (*_gc)(void);
|
||||
void (*_shutdown)(bool full, bool silent);
|
||||
} custom_heap;
|
||||
HashTable *tracked_allocs;
|
||||
union {
|
||||
HashTable *tracked_allocs;
|
||||
struct {
|
||||
bool poison_alloc;
|
||||
uint8_t poison_alloc_value;
|
||||
bool poison_free;
|
||||
uint8_t poison_free_value;
|
||||
uint8_t padding;
|
||||
bool check_freelists_on_shutdown;
|
||||
} debug;
|
||||
};
|
||||
#endif
|
||||
pid_t pid;
|
||||
zend_random_bytes_insecure_state rand_state;
|
||||
@@ -2389,8 +2399,19 @@ static void zend_mm_check_leaks(zend_mm_heap *heap)
|
||||
#if ZEND_MM_CUSTOM
|
||||
static void *tracked_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
static void tracked_free_all(zend_mm_heap *heap);
|
||||
static void *poison_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
#endif
|
||||
|
||||
static void zend_mm_check_freelists(zend_mm_heap *heap)
|
||||
{
|
||||
for (uint32_t bin_num = 0; bin_num < ZEND_MM_BINS; bin_num++) {
|
||||
zend_mm_free_slot *slot = heap->free_slot[bin_num];
|
||||
while (slot) {
|
||||
slot = zend_mm_get_next_free_slot(heap, bin_num, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
|
||||
{
|
||||
zend_mm_chunk *p;
|
||||
@@ -2555,8 +2576,9 @@ ZEND_API size_t ZEND_FASTCALL _zend_mm_block_size(zend_mm_heap *heap, void *ptr
|
||||
if (size_zv) {
|
||||
return Z_LVAL_P(size_zv);
|
||||
}
|
||||
} else if (heap->custom_heap._malloc != poison_malloc) {
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
return zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
@@ -3021,6 +3043,200 @@ static void tracked_free_all(zend_mm_heap *heap) {
|
||||
}
|
||||
#endif
|
||||
|
||||
static void* poison_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
|
||||
{
|
||||
zend_mm_heap *heap = AG(mm_heap);
|
||||
|
||||
if (SIZE_MAX - heap->debug.padding * 2 < size) {
|
||||
zend_mm_panic("Integer overflow in memory allocation");
|
||||
}
|
||||
size += heap->debug.padding * 2;
|
||||
|
||||
void *ptr = zend_mm_alloc_heap(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
|
||||
if (EXPECTED(ptr)) {
|
||||
if (heap->debug.poison_alloc) {
|
||||
memset(ptr, heap->debug.poison_alloc_value, size);
|
||||
}
|
||||
|
||||
ptr = (char*)ptr + heap->debug.padding;
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void poison_free(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
|
||||
{
|
||||
zend_mm_heap *heap = AG(mm_heap);
|
||||
|
||||
if (EXPECTED(ptr)) {
|
||||
/* zend_mm_shutdown() will try to free the heap when custom handlers
|
||||
* are installed */
|
||||
if (UNEXPECTED(ptr == heap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ptr = (char*)ptr - heap->debug.padding;
|
||||
|
||||
size_t size = zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
|
||||
if (heap->debug.poison_free) {
|
||||
memset(ptr, heap->debug.poison_free_value, size);
|
||||
}
|
||||
}
|
||||
|
||||
zend_mm_free_heap(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
}
|
||||
|
||||
static void* poison_realloc(void *ptr, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
|
||||
{
|
||||
zend_mm_heap *heap = AG(mm_heap);
|
||||
|
||||
void *new = poison_malloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
|
||||
if (ptr) {
|
||||
/* Determine the size of the old allocation from the unpadded pointer. */
|
||||
size_t oldsize = zend_mm_size(heap, (char*)ptr - heap->debug.padding ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
|
||||
/* Remove the padding size to determine the size that is available to the user. */
|
||||
oldsize -= (2 * heap->debug.padding);
|
||||
|
||||
#if ZEND_DEBUG
|
||||
oldsize -= sizeof(zend_mm_debug_info);
|
||||
#endif
|
||||
|
||||
memcpy(new, ptr, MIN(oldsize, size));
|
||||
poison_free(ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
|
||||
}
|
||||
|
||||
return new;
|
||||
}
|
||||
|
||||
static size_t poison_gc(void)
|
||||
{
|
||||
zend_mm_heap *heap = AG(mm_heap);
|
||||
|
||||
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
size_t (*_gc)(void);
|
||||
void (*_shutdown)(bool, bool);
|
||||
|
||||
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
|
||||
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
size_t collected = zend_mm_gc(heap);
|
||||
|
||||
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
static void poison_shutdown(bool full, bool silent)
|
||||
{
|
||||
zend_mm_heap *heap = AG(mm_heap);
|
||||
|
||||
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
|
||||
size_t (*_gc)(void);
|
||||
void (*_shutdown)(bool, bool);
|
||||
|
||||
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
|
||||
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
|
||||
|
||||
if (heap->debug.check_freelists_on_shutdown) {
|
||||
zend_mm_check_freelists(heap);
|
||||
}
|
||||
|
||||
zend_mm_shutdown(heap, full, silent);
|
||||
|
||||
if (!full) {
|
||||
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
static void poison_enable(zend_mm_heap *heap, char *parameters)
|
||||
{
|
||||
char *tmp = parameters;
|
||||
char *end = tmp + strlen(tmp);
|
||||
|
||||
/* Trim heading/trailing whitespaces */
|
||||
while (*tmp == ' ' || *tmp == '\t' || *tmp == '\n') {
|
||||
tmp++;
|
||||
}
|
||||
while (end != tmp && (*(end-1) == ' ' || *(end-1) == '\t' || *(end-1) == '\n')) {
|
||||
end--;
|
||||
}
|
||||
|
||||
if (tmp == end) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
char *key = tmp;
|
||||
|
||||
tmp = memchr(tmp, '=', end - tmp);
|
||||
if (!tmp) {
|
||||
size_t key_len = end - key;
|
||||
fprintf(stderr, "Unexpected EOF after ZEND_MM_DEBUG parameter '%.*s', expected '='\n",
|
||||
(int)key_len, key);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t key_len = tmp - key;
|
||||
char *value = tmp + 1;
|
||||
|
||||
if (key_len == strlen("poison_alloc")
|
||||
&& !memcmp(key, "poison_alloc", key_len)) {
|
||||
|
||||
heap->debug.poison_alloc = true;
|
||||
heap->debug.poison_alloc_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
|
||||
|
||||
} else if (key_len == strlen("poison_free")
|
||||
&& !memcmp(key, "poison_free", key_len)) {
|
||||
|
||||
heap->debug.poison_free = true;
|
||||
heap->debug.poison_free_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
|
||||
|
||||
} else if (key_len == strlen("padding")
|
||||
&& !memcmp(key, "padding", key_len)) {
|
||||
|
||||
uint8_t padding = ZEND_STRTOUL(value, &tmp, 0);
|
||||
if (ZEND_MM_ALIGNED_SIZE(padding) != padding) {
|
||||
fprintf(stderr, "ZEND_MM_DEBUG padding must be a multiple of %u, %u given\n",
|
||||
(unsigned int)ZEND_MM_ALIGNMENT,
|
||||
(unsigned int)padding);
|
||||
return;
|
||||
}
|
||||
heap->debug.padding = padding;
|
||||
|
||||
} else if (key_len == strlen("check_freelists_on_shutdown")
|
||||
&& !memcmp(key, "check_freelists_on_shutdown", key_len)) {
|
||||
|
||||
heap->debug.check_freelists_on_shutdown = (bool) ZEND_STRTOUL(value, &tmp, 0);
|
||||
|
||||
} else {
|
||||
fprintf(stderr, "Unknown ZEND_MM_DEBUG parameter: '%.*s'\n",
|
||||
(int)key_len, key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tmp == end) {
|
||||
break;
|
||||
}
|
||||
if (*tmp != ',') {
|
||||
fprintf(stderr, "Unexpected '%c' after value of ZEND_MM_DEBUG parameter '%.*s', expected ','\n",
|
||||
*tmp, (int)key_len, key);
|
||||
return;
|
||||
}
|
||||
tmp++;
|
||||
}
|
||||
|
||||
zend_mm_set_custom_handlers_ex(heap, poison_malloc, poison_free,
|
||||
poison_realloc, poison_gc, poison_shutdown);
|
||||
}
|
||||
|
||||
static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
|
||||
{
|
||||
char *tmp;
|
||||
@@ -3057,6 +3273,14 @@ static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
|
||||
zend_mm_use_huge_pages = true;
|
||||
}
|
||||
alloc_globals->mm_heap = zend_mm_init();
|
||||
|
||||
#if ZEND_MM_CUSTOM
|
||||
ZEND_ASSERT(!alloc_globals->mm_heap->tracked_allocs);
|
||||
tmp = getenv("ZEND_MM_DEBUG");
|
||||
if (tmp) {
|
||||
poison_enable(alloc_globals->mm_heap, tmp);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ZTS
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
#include "zend_modules.h"
|
||||
#include "zend_types.h"
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include "config.h"
|
||||
#endif
|
||||
@@ -182,6 +183,19 @@ static ZEND_FUNCTION(zend_leak_variable)
|
||||
Z_ADDREF_P(zv);
|
||||
}
|
||||
|
||||
static ZEND_FUNCTION(zend_delref)
|
||||
{
|
||||
zval *zv;
|
||||
|
||||
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv) == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
Z_TRY_DELREF_P(zv);
|
||||
|
||||
RETURN_NULL();
|
||||
}
|
||||
|
||||
/* Tests Z_PARAM_OBJ_OR_STR */
|
||||
static ZEND_FUNCTION(zend_string_or_object)
|
||||
{
|
||||
|
||||
@@ -235,6 +235,8 @@ namespace {
|
||||
|
||||
function zend_leak_bytes(int $bytes = 3): void {}
|
||||
|
||||
function zend_delref(mixed $variable): void {}
|
||||
|
||||
function zend_string_or_object(object|string $param): object|string {}
|
||||
|
||||
function zend_string_or_object_or_null(object|string|null $param): object|string|null {}
|
||||
|
||||
6
ext/zend_test/test_arginfo.h
generated
6
ext/zend_test/test_arginfo.h
generated
@@ -1,5 +1,5 @@
|
||||
/* This is a generated file, edit the .stub.php file instead.
|
||||
* Stub hash: 3082e62e96d5f4383c98638513463c676a7c3a69 */
|
||||
* Stub hash: 80b2dbc373baccd5ee4df047070d95e4c44effcd */
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
@@ -42,6 +42,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_leak_bytes, 0, 0, IS_VOID,
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, bytes, IS_LONG, 0, "3")
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
#define arginfo_zend_delref arginfo_zend_leak_variable
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_zend_string_or_object, 0, 1, MAY_BE_OBJECT|MAY_BE_STRING)
|
||||
ZEND_ARG_TYPE_MASK(0, param, MAY_BE_OBJECT|MAY_BE_STRING, NULL)
|
||||
ZEND_END_ARG_INFO()
|
||||
@@ -264,6 +266,7 @@ static ZEND_FUNCTION(zend_create_unterminated_string);
|
||||
static ZEND_FUNCTION(zend_terminate_string);
|
||||
static ZEND_FUNCTION(zend_leak_variable);
|
||||
static ZEND_FUNCTION(zend_leak_bytes);
|
||||
static ZEND_FUNCTION(zend_delref);
|
||||
static ZEND_FUNCTION(zend_string_or_object);
|
||||
static ZEND_FUNCTION(zend_string_or_object_or_null);
|
||||
static ZEND_FUNCTION(zend_string_or_stdclass);
|
||||
@@ -369,6 +372,7 @@ static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(zend_terminate_string, arginfo_zend_terminate_string)
|
||||
ZEND_FE(zend_leak_variable, arginfo_zend_leak_variable)
|
||||
ZEND_FE(zend_leak_bytes, arginfo_zend_leak_bytes)
|
||||
ZEND_FE(zend_delref, arginfo_zend_delref)
|
||||
ZEND_FE(zend_string_or_object, arginfo_zend_string_or_object)
|
||||
ZEND_FE(zend_string_or_object_or_null, arginfo_zend_string_or_object_or_null)
|
||||
ZEND_FE(zend_string_or_stdclass, arginfo_zend_string_or_stdclass)
|
||||
|
||||
@@ -6,6 +6,12 @@ https://github.com/php/php-src/pull/12758
|
||||
zend_test
|
||||
--ENV--
|
||||
USE_ZEND_ALLOC=1
|
||||
--SKIPIF--
|
||||
<?php
|
||||
if (getenv("ZEND_MM_DEBUG")) {
|
||||
die("skip zend_test.observe_opline_in_zendmm not compatible with ZEND_MM_DEBUG");
|
||||
}
|
||||
?>
|
||||
--INI--
|
||||
zend_test.observe_opline_in_zendmm=1
|
||||
--FILE--
|
||||
|
||||
@@ -4,6 +4,12 @@ possible segfault in `ZEND_FUNC_GET_ARGS`
|
||||
zend_test
|
||||
--ENV--
|
||||
USE_ZEND_ALLOC=1
|
||||
--SKIPIF--
|
||||
<?php
|
||||
if (getenv("ZEND_MM_DEBUG")) {
|
||||
die("skip zend_test.observe_opline_in_zendmm not compatible with ZEND_MM_DEBUG");
|
||||
}
|
||||
?>
|
||||
--INI--
|
||||
zend_test.observe_opline_in_zendmm=1
|
||||
--FILE--
|
||||
|
||||
Reference in New Issue
Block a user