Skip to content

Commit cb24541

Browse files
arnaud-lbTimWolla
andauthored
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] #14054 Co-authored-by: Tim Düsterhus <timwolla@googlemail.com>
1 parent 334d9bb commit cb24541

File tree

6 files changed

+259
-3
lines changed

6 files changed

+259
-3
lines changed

Zend/zend_alloc.c

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,17 @@ struct _zend_mm_heap {
305305
size_t (*_gc)(void);
306306
void (*_shutdown)(bool full, bool silent);
307307
} custom_heap;
308-
HashTable *tracked_allocs;
308+
union {
309+
HashTable *tracked_allocs;
310+
struct {
311+
bool poison_alloc;
312+
uint8_t poison_alloc_value;
313+
bool poison_free;
314+
uint8_t poison_free_value;
315+
uint8_t padding;
316+
bool check_freelists_on_shutdown;
317+
} debug;
318+
};
309319
#endif
310320
pid_t pid;
311321
zend_random_bytes_insecure_state rand_state;
@@ -2389,8 +2399,19 @@ static void zend_mm_check_leaks(zend_mm_heap *heap)
23892399
#if ZEND_MM_CUSTOM
23902400
static void *tracked_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
23912401
static void tracked_free_all(zend_mm_heap *heap);
2402+
static void *poison_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
23922403
#endif
23932404

2405+
static void zend_mm_check_freelists(zend_mm_heap *heap)
2406+
{
2407+
for (uint32_t bin_num = 0; bin_num < ZEND_MM_BINS; bin_num++) {
2408+
zend_mm_free_slot *slot = heap->free_slot[bin_num];
2409+
while (slot) {
2410+
slot = zend_mm_get_next_free_slot(heap, bin_num, slot);
2411+
}
2412+
}
2413+
}
2414+
23942415
ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
23952416
{
23962417
zend_mm_chunk *p;
@@ -2555,8 +2576,9 @@ ZEND_API size_t ZEND_FASTCALL _zend_mm_block_size(zend_mm_heap *heap, void *ptr
25552576
if (size_zv) {
25562577
return Z_LVAL_P(size_zv);
25572578
}
2579+
} else if (heap->custom_heap._malloc != poison_malloc) {
2580+
return 0;
25582581
}
2559-
return 0;
25602582
}
25612583
#endif
25622584
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) {
30213043
}
30223044
#endif
30233045

3046+
static void* poison_malloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
3047+
{
3048+
zend_mm_heap *heap = AG(mm_heap);
3049+
3050+
if (SIZE_MAX - heap->debug.padding * 2 < size) {
3051+
zend_mm_panic("Integer overflow in memory allocation");
3052+
}
3053+
size += heap->debug.padding * 2;
3054+
3055+
void *ptr = zend_mm_alloc_heap(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3056+
3057+
if (EXPECTED(ptr)) {
3058+
if (heap->debug.poison_alloc) {
3059+
memset(ptr, heap->debug.poison_alloc_value, size);
3060+
}
3061+
3062+
ptr = (char*)ptr + heap->debug.padding;
3063+
}
3064+
3065+
return ptr;
3066+
}
3067+
3068+
static void poison_free(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
3069+
{
3070+
zend_mm_heap *heap = AG(mm_heap);
3071+
3072+
if (EXPECTED(ptr)) {
3073+
/* zend_mm_shutdown() will try to free the heap when custom handlers
3074+
* are installed */
3075+
if (UNEXPECTED(ptr == heap)) {
3076+
return;
3077+
}
3078+
3079+
ptr = (char*)ptr - heap->debug.padding;
3080+
3081+
size_t size = zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3082+
3083+
if (heap->debug.poison_free) {
3084+
memset(ptr, heap->debug.poison_free_value, size);
3085+
}
3086+
}
3087+
3088+
zend_mm_free_heap(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3089+
}
3090+
3091+
static void* poison_realloc(void *ptr, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
3092+
{
3093+
zend_mm_heap *heap = AG(mm_heap);
3094+
3095+
void *new = poison_malloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3096+
3097+
if (ptr) {
3098+
/* Determine the size of the old allocation from the unpadded pointer. */
3099+
size_t oldsize = zend_mm_size(heap, (char*)ptr - heap->debug.padding ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3100+
3101+
/* Remove the padding size to determine the size that is available to the user. */
3102+
oldsize -= (2 * heap->debug.padding);
3103+
3104+
#if ZEND_DEBUG
3105+
oldsize -= sizeof(zend_mm_debug_info);
3106+
#endif
3107+
3108+
memcpy(new, ptr, MIN(oldsize, size));
3109+
poison_free(ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3110+
}
3111+
3112+
return new;
3113+
}
3114+
3115+
static size_t poison_gc(void)
3116+
{
3117+
zend_mm_heap *heap = AG(mm_heap);
3118+
3119+
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3120+
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3121+
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3122+
size_t (*_gc)(void);
3123+
void (*_shutdown)(bool, bool);
3124+
3125+
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
3126+
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
3127+
3128+
size_t collected = zend_mm_gc(heap);
3129+
3130+
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
3131+
3132+
return collected;
3133+
}
3134+
3135+
static void poison_shutdown(bool full, bool silent)
3136+
{
3137+
zend_mm_heap *heap = AG(mm_heap);
3138+
3139+
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3140+
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3141+
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3142+
size_t (*_gc)(void);
3143+
void (*_shutdown)(bool, bool);
3144+
3145+
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
3146+
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
3147+
3148+
if (heap->debug.check_freelists_on_shutdown) {
3149+
zend_mm_check_freelists(heap);
3150+
}
3151+
3152+
zend_mm_shutdown(heap, full, silent);
3153+
3154+
if (!full) {
3155+
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
3156+
}
3157+
}
3158+
3159+
static void poison_enable(zend_mm_heap *heap, char *parameters)
3160+
{
3161+
char *tmp = parameters;
3162+
char *end = tmp + strlen(tmp);
3163+
3164+
/* Trim heading/trailing whitespaces */
3165+
while (*tmp == ' ' || *tmp == '\t' || *tmp == '\n') {
3166+
tmp++;
3167+
}
3168+
while (end != tmp && (*(end-1) == ' ' || *(end-1) == '\t' || *(end-1) == '\n')) {
3169+
end--;
3170+
}
3171+
3172+
if (tmp == end) {
3173+
return;
3174+
}
3175+
3176+
while (1) {
3177+
char *key = tmp;
3178+
3179+
tmp = memchr(tmp, '=', end - tmp);
3180+
if (!tmp) {
3181+
size_t key_len = end - key;
3182+
fprintf(stderr, "Unexpected EOF after ZEND_MM_DEBUG parameter '%.*s', expected '='\n",
3183+
(int)key_len, key);
3184+
return;
3185+
}
3186+
3187+
size_t key_len = tmp - key;
3188+
char *value = tmp + 1;
3189+
3190+
if (key_len == strlen("poison_alloc")
3191+
&& !memcmp(key, "poison_alloc", key_len)) {
3192+
3193+
heap->debug.poison_alloc = true;
3194+
heap->debug.poison_alloc_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
3195+
3196+
} else if (key_len == strlen("poison_free")
3197+
&& !memcmp(key, "poison_free", key_len)) {
3198+
3199+
heap->debug.poison_free = true;
3200+
heap->debug.poison_free_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
3201+
3202+
} else if (key_len == strlen("padding")
3203+
&& !memcmp(key, "padding", key_len)) {
3204+
3205+
uint8_t padding = ZEND_STRTOUL(value, &tmp, 0);
3206+
if (ZEND_MM_ALIGNED_SIZE(padding) != padding) {
3207+
fprintf(stderr, "ZEND_MM_DEBUG padding must be a multiple of %u, %u given\n",
3208+
(unsigned int)ZEND_MM_ALIGNMENT,
3209+
(unsigned int)padding);
3210+
return;
3211+
}
3212+
heap->debug.padding = padding;
3213+
3214+
} else if (key_len == strlen("check_freelists_on_shutdown")
3215+
&& !memcmp(key, "check_freelists_on_shutdown", key_len)) {
3216+
3217+
heap->debug.check_freelists_on_shutdown = (bool) ZEND_STRTOUL(value, &tmp, 0);
3218+
3219+
} else {
3220+
fprintf(stderr, "Unknown ZEND_MM_DEBUG parameter: '%.*s'\n",
3221+
(int)key_len, key);
3222+
return;
3223+
}
3224+
3225+
if (tmp == end) {
3226+
break;
3227+
}
3228+
if (*tmp != ',') {
3229+
fprintf(stderr, "Unexpected '%c' after value of ZEND_MM_DEBUG parameter '%.*s', expected ','\n",
3230+
*tmp, (int)key_len, key);
3231+
return;
3232+
}
3233+
tmp++;
3234+
}
3235+
3236+
zend_mm_set_custom_handlers_ex(heap, poison_malloc, poison_free,
3237+
poison_realloc, poison_gc, poison_shutdown);
3238+
}
3239+
30243240
static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
30253241
{
30263242
char *tmp;
@@ -3057,6 +3273,14 @@ static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
30573273
zend_mm_use_huge_pages = true;
30583274
}
30593275
alloc_globals->mm_heap = zend_mm_init();
3276+
3277+
#if ZEND_MM_CUSTOM
3278+
ZEND_ASSERT(!alloc_globals->mm_heap->tracked_allocs);
3279+
tmp = getenv("ZEND_MM_DEBUG");
3280+
if (tmp) {
3281+
poison_enable(alloc_globals->mm_heap, tmp);
3282+
}
3283+
#endif
30603284
}
30613285

30623286
#ifdef ZTS

ext/zend_test/test.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#include "zend_modules.h"
18+
#include "zend_types.h"
1819
#ifdef HAVE_CONFIG_H
1920
# include "config.h"
2021
#endif
@@ -182,6 +183,19 @@ static ZEND_FUNCTION(zend_leak_variable)
182183
Z_ADDREF_P(zv);
183184
}
184185

186+
static ZEND_FUNCTION(zend_delref)
187+
{
188+
zval *zv;
189+
190+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv) == FAILURE) {
191+
RETURN_THROWS();
192+
}
193+
194+
Z_TRY_DELREF_P(zv);
195+
196+
RETURN_NULL();
197+
}
198+
185199
/* Tests Z_PARAM_OBJ_OR_STR */
186200
static ZEND_FUNCTION(zend_string_or_object)
187201
{

ext/zend_test/test.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ function zend_leak_variable(mixed $variable): void {}
235235

236236
function zend_leak_bytes(int $bytes = 3): void {}
237237

238+
function zend_delref(mixed $variable): void {}
239+
238240
function zend_string_or_object(object|string $param): object|string {}
239241

240242
function zend_string_or_object_or_null(object|string|null $param): object|string|null {}

ext/zend_test/test_arginfo.h

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/zend_test/tests/opline_dangling.phpt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ https://github.com/php/php-src/pull/12758
66
zend_test
77
--ENV--
88
USE_ZEND_ALLOC=1
9+
--SKIPIF--
10+
<?php
11+
if (getenv("ZEND_MM_DEBUG")) {
12+
die("skip zend_test.observe_opline_in_zendmm not compatible with ZEND_MM_DEBUG");
13+
}
14+
?>
915
--INI--
1016
zend_test.observe_opline_in_zendmm=1
1117
--FILE--

ext/zend_test/tests/opline_dangling_02.phpt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ possible segfault in `ZEND_FUNC_GET_ARGS`
44
zend_test
55
--ENV--
66
USE_ZEND_ALLOC=1
7+
--SKIPIF--
8+
<?php
9+
if (getenv("ZEND_MM_DEBUG")) {
10+
die("skip zend_test.observe_opline_in_zendmm not compatible with ZEND_MM_DEBUG");
11+
}
12+
?>
713
--INI--
814
zend_test.observe_opline_in_zendmm=1
915
--FILE--

0 commit comments

Comments
 (0)