Skip to content

Commit 2c504bc

Browse files
committed
Add heap runtime-enabled heap debugging capabilities
1 parent a519a03 commit 2c504bc

File tree

4 files changed

+300
-3
lines changed

4 files changed

+300
-3
lines changed

Zend/zend_alloc.c

+279-2
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 (int 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,253 @@ 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+
if (SIZE_MAX - heap->debug.padding * 2 < size) {
3096+
zend_mm_panic("Integer overflow in memory allocation");
3097+
}
3098+
size += heap->debug.padding * 2;
3099+
3100+
size_t oldsize;
3101+
if (ptr) {
3102+
ptr = (char*)ptr - heap->debug.padding;
3103+
oldsize = zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3104+
3105+
if (oldsize > size) {
3106+
if (heap->debug.poison_free) {
3107+
memset((char*)ptr + size, heap->debug.poison_free_value, oldsize - size);
3108+
}
3109+
}
3110+
} else {
3111+
oldsize = 0;
3112+
}
3113+
3114+
void *old_ptr = ptr;
3115+
ptr = zend_mm_realloc_heap(heap, ptr, size, 0, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3116+
3117+
if (EXPECTED(ptr)) {
3118+
size = zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
3119+
if (old_ptr != ptr) {
3120+
if (old_ptr) {
3121+
/* old ptr was freed */
3122+
if (heap->debug.poison_free && oldsize <= ZEND_MM_MAX_SMALL_SIZE) {
3123+
#if ZEND_DEBUG
3124+
size_t len = oldsize - sizeof(zend_mm_free_slot*) - sizeof(zend_mm_debug_info);
3125+
#else
3126+
size_t len = oldsize - sizeof(zend_mm_free_slot*) * 2;
3127+
#endif
3128+
memset((char*)old_ptr + sizeof(zend_mm_free_slot*), heap->debug.poison_free_value, len);
3129+
}
3130+
}
3131+
/* ptr is a new allocation */
3132+
if (heap->debug.poison_alloc) {
3133+
if (oldsize == 0) {
3134+
#if ZEND_DEBUG
3135+
memset(ptr, heap->debug.poison_alloc_value, size - sizeof(zend_mm_debug_info));
3136+
#else
3137+
memset(ptr, heap->debug.poison_alloc_value, size);
3138+
#endif
3139+
/* old content was copied to ptr, don't override it */
3140+
} else if (size > oldsize) {
3141+
#if ZEND_DEBUG
3142+
memset((char*)ptr + oldsize - sizeof(zend_mm_debug_info), heap->debug.poison_alloc_value, size - oldsize);
3143+
#else
3144+
memset((char*)ptr + oldsize, heap->debug.poison_alloc_value, size - oldsize);
3145+
#endif
3146+
}
3147+
}
3148+
} else if (size > oldsize) {
3149+
if (heap->debug.poison_alloc) {
3150+
#if ZEND_DEBUG
3151+
memset((char*)ptr + oldsize - sizeof(zend_mm_debug_info), heap->debug.poison_alloc_value, size - oldsize);
3152+
#else
3153+
memset((char*)ptr + oldsize, heap->debug.poison_alloc_value, size - oldsize);
3154+
#endif
3155+
}
3156+
} else if (size < oldsize) {
3157+
if (heap->debug.poison_free) {
3158+
#if ZEND_DEBUG
3159+
memset((char*)ptr + size - sizeof(zend_mm_debug_info), heap->debug.poison_free_value, oldsize - size);
3160+
#else
3161+
memset((char*)ptr + size, heap->debug.poison_free_value, oldsize - size);
3162+
#endif
3163+
}
3164+
}
3165+
3166+
ptr = (char*)ptr + heap->debug.padding;
3167+
}
3168+
3169+
return ptr;
3170+
}
3171+
3172+
static size_t poison_gc(void)
3173+
{
3174+
zend_mm_heap *heap = AG(mm_heap);
3175+
3176+
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3177+
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3178+
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3179+
size_t (*_gc)(void);
3180+
void (*_shutdown)(bool, bool);
3181+
3182+
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
3183+
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
3184+
3185+
size_t collected = zend_mm_gc(heap);
3186+
3187+
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
3188+
3189+
return collected;
3190+
}
3191+
3192+
static void poison_shutdown(bool full, bool silent)
3193+
{
3194+
zend_mm_heap *heap = AG(mm_heap);
3195+
3196+
void* (*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3197+
void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3198+
void* (*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
3199+
size_t (*_gc)(void);
3200+
void (*_shutdown)(bool, bool);
3201+
3202+
zend_mm_get_custom_handlers_ex(heap, &_malloc, &_free, &_realloc, &_gc, &_shutdown);
3203+
zend_mm_set_custom_handlers_ex(heap, NULL, NULL, NULL, NULL, NULL);
3204+
3205+
if (heap->debug.check_freelists_on_shutdown) {
3206+
zend_mm_check_freelists(heap);
3207+
}
3208+
3209+
zend_mm_shutdown(heap, full, silent);
3210+
3211+
if (!full) {
3212+
zend_mm_set_custom_handlers_ex(heap, _malloc, _free, _realloc, _gc, _shutdown);
3213+
}
3214+
}
3215+
3216+
static void poison_enable(zend_mm_heap *heap, char *parameters)
3217+
{
3218+
char *tmp = parameters;
3219+
char *end = tmp + strlen(tmp);
3220+
3221+
/* Trim heading/trailing whitespaces */
3222+
while (*tmp == ' ' || *tmp == '\t' || *tmp == '\n') {
3223+
tmp++;
3224+
}
3225+
while (end != tmp && (*(end-1) == ' ' || *(end-1) == '\t' || *(end-1) == '\n')) {
3226+
end--;
3227+
}
3228+
3229+
while (1) {
3230+
char *key = tmp;
3231+
3232+
tmp = memchr(tmp, '=', end - tmp);
3233+
if (!tmp) {
3234+
size_t key_len = end - key;
3235+
fprintf(stderr, "Unexpected EOF after ZEND_MM_DEBUG parameter '%.*s', expected '='\n",
3236+
(int)key_len, key);
3237+
return;
3238+
}
3239+
3240+
size_t key_len = tmp - key;
3241+
char *value = tmp + 1;
3242+
3243+
if (key_len == strlen("poison_alloc")
3244+
&& !memcmp(key, "poison_alloc", key_len)) {
3245+
3246+
heap->debug.poison_alloc = true;
3247+
heap->debug.poison_alloc_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
3248+
3249+
} else if (key_len == strlen("poison_free")
3250+
&& !memcmp(key, "poison_free", key_len)) {
3251+
3252+
heap->debug.poison_free = true;
3253+
heap->debug.poison_free_value = (uint8_t) ZEND_STRTOUL(value, &tmp, 0);
3254+
3255+
} else if (key_len == strlen("padding")
3256+
&& !memcmp(key, "padding", key_len)) {
3257+
3258+
uint8_t padding = ZEND_STRTOUL(value, &tmp, 0);
3259+
if (ZEND_MM_ALIGNED_SIZE(padding) != padding) {
3260+
fprintf(stderr, "ZEND_MM_DEBUG padding must be a multiple of %u, %u given\n",
3261+
(unsigned int)ZEND_MM_ALIGNMENT,
3262+
(unsigned int)padding);
3263+
return;
3264+
}
3265+
heap->debug.padding = padding;
3266+
3267+
} else if (key_len == strlen("check_freelists_on_shutdown")
3268+
&& !memcmp(key, "check_freelists_on_shutdown", key_len)) {
3269+
3270+
heap->debug.check_freelists_on_shutdown = (bool) ZEND_STRTOUL(value, &tmp, 0);
3271+
3272+
} else {
3273+
fprintf(stderr, "Unknown ZEND_MM_DEBUG parameter: '%.*s'\n",
3274+
(int)key_len, key);
3275+
return;
3276+
}
3277+
3278+
if (tmp == end) {
3279+
break;
3280+
}
3281+
if (*tmp != ',') {
3282+
fprintf(stderr, "Unexpected '%c' after value of ZEND_MM_DEBUG parameter '%.*s', expected ','\n",
3283+
*tmp, (int)key_len, key);
3284+
return;
3285+
}
3286+
tmp++;
3287+
}
3288+
3289+
zend_mm_set_custom_handlers_ex(heap, poison_malloc, poison_free,
3290+
poison_realloc, poison_gc, poison_shutdown);
3291+
}
3292+
30243293
static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
30253294
{
30263295
char *tmp;
@@ -3057,6 +3326,14 @@ static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
30573326
zend_mm_use_huge_pages = true;
30583327
}
30593328
alloc_globals->mm_heap = zend_mm_init();
3329+
3330+
#if ZEND_MM_CUSTOM
3331+
ZEND_ASSERT(!alloc_globals->mm_heap->tracked_allocs);
3332+
tmp = getenv("ZEND_MM_DEBUG");
3333+
if (tmp) {
3334+
poison_enable(alloc_globals->mm_heap, tmp);
3335+
}
3336+
#endif
30603337
}
30613338

30623339
#ifdef ZTS

ext/zend_test/test.c

+14
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

+2
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

+5-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)