1
0
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:
Arnaud Le Blanc
2025-03-31 17:24:42 +02:00
committed by GitHub
parent 334d9bbc09
commit cb245411b0
6 changed files with 259 additions and 3 deletions

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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 {}

View File

@@ -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)

View File

@@ -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--

View 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--