From 5a6c4e6e36083c1c6623b48ca334318ea874114b Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 15:28:37 +0300 Subject: [PATCH 1/8] Prevent hp_clean_profiler_state from clearing already freed memory - The 1 (third parameter) means "destroy the original zval after copying" - This transfers ownership completely to the return value - XHPROF_G(stats_count) becomes IS_UNDEF - During cleanup, the check if (Z_TYPE(XHPROF_G(stats_count)) != IS_UNDEF) fails - No double-free occurs --- extension/xhprof.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/xhprof.c b/extension/xhprof.c index e91f6c90..2e685a13 100755 --- a/extension/xhprof.c +++ b/extension/xhprof.c @@ -184,7 +184,7 @@ PHP_FUNCTION(xhprof_disable) { if (XHPROF_G(enabled)) { hp_stop(); - RETURN_ZVAL(&XHPROF_G(stats_count), 1, 0); + RETURN_ZVAL(&XHPROF_G(stats_count), 1, 1); } /* else null is returned */ } @@ -214,7 +214,7 @@ PHP_FUNCTION(xhprof_sample_disable) { if (XHPROF_G(enabled)) { hp_stop(); - RETURN_ZVAL(&XHPROF_G(stats_count), 1, 0); + RETURN_ZVAL(&XHPROF_G(stats_count), 1, 1); } /* else null is returned */ } From 620597be0911b9e7fa530a6cf5dbc2984a6c7935 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 16:04:00 +0300 Subject: [PATCH 2/8] Fix hash counter underflow 1. Ignored functions in PHP 8.0+ create entries with is_trace = 0 2. These entries skip hp_mode_common_beginfn() so the hash counter is never incremented 3. But hp_mode_hier_endfn_cb() was always decrementing the hash counter 4. This caused hash counter underflow, which could lead to memory corruption and segfaults --- extension/xhprof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/xhprof.c b/extension/xhprof.c index 2e685a13..60a86705 100755 --- a/extension/xhprof.c +++ b/extension/xhprof.c @@ -924,7 +924,7 @@ void hp_mode_hier_endfn_cb(hp_entry_t **entries) #if PHP_VERSION_ID >= 80000 if (top->is_trace == 0) { - XHPROF_G(func_hash_counters[top->hash_code])--; + /* For ignored functions, don't decrement hash counter since it was never incremented */ return; } #endif From 34c830c28d9adaacbfea695d46bff8b22f3b5b98 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 16:12:24 +0300 Subject: [PATCH 3/8] Always release the function name If a callback is found and executed, it calls zend_string_release(function_name) and returns trace_name. However, if no callback is found, it returns function_name directly without releasing it. --- extension/trace.h | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/extension/trace.h b/extension/trace.h index f690b18c..49e65095 100644 --- a/extension/trace.h +++ b/extension/trace.h @@ -84,16 +84,13 @@ static zend_always_inline zend_string *hp_get_trace_callback(zend_string *functi callback = (hp_trace_callback*)zend_hash_find_ptr(XHPROF_G(trace_callbacks), function_name); if (callback) { trace_name = (*callback)(function_name, data); - } else { - return function_name; + zend_string_release(function_name); + return trace_name; } - } else { - return function_name; } - - zend_string_release(function_name); - - return trace_name; + + /* No callback found, return the original function_name */ + return function_name; } static zend_always_inline hp_entry_t *hp_fast_alloc_hprof_entry() From 7e06de7248363d53bece7cb0e3c35702bca1215e Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 15:52:45 +0300 Subject: [PATCH 4/8] Simplify bloom filter to avoid collisions --- extension/xhprof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/xhprof.c b/extension/xhprof.c index 60a86705..ddef0b36 100755 --- a/extension/xhprof.c +++ b/extension/xhprof.c @@ -462,7 +462,7 @@ hp_ignored_functions *hp_ignored_functions_init(zval *values) for (; names[i] != NULL; i++) { zend_ulong hash = ZSTR_HASH(names[i]); int idx = hash % XHPROF_MAX_IGNORED_FUNCTIONS; - functions->filter[idx] = hash; + functions->filter[idx] = 1; /* Set to 1 to indicate presence, not the hash value */ } return functions; From aed275047e137a210c227eadb969d19ca695aeef Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 16:21:04 +0300 Subject: [PATCH 5/8] Revert "Simplify bloom filter to avoid collisions" This reverts commit 7e06de7248363d53bece7cb0e3c35702bca1215e. --- extension/xhprof.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/xhprof.c b/extension/xhprof.c index ddef0b36..60a86705 100755 --- a/extension/xhprof.c +++ b/extension/xhprof.c @@ -462,7 +462,7 @@ hp_ignored_functions *hp_ignored_functions_init(zval *values) for (; names[i] != NULL; i++) { zend_ulong hash = ZSTR_HASH(names[i]); int idx = hash % XHPROF_MAX_IGNORED_FUNCTIONS; - functions->filter[idx] = 1; /* Set to 1 to indicate presence, not the hash value */ + functions->filter[idx] = hash; } return functions; From 62aaf931b19101e51cccce41864f941c65eea029 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 16:36:24 +0300 Subject: [PATCH 6/8] Add NULL check for ignored functions entry creation in PHP 8.0+ Fixes segmentation fault when the first function call in a request is an ignored function. Previously, the code assumed *(entries) was always non-NULL when creating entries for ignored functions, but this caused a NULL pointer dereference when profiling started with an ignored function. The fix adds a NULL check and uses the actual function name when entries is NULL, ensuring proper initialization of the entry stack even when the first call is ignored. --- extension/trace.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/extension/trace.h b/extension/trace.h index 49e65095..7e1fd124 100644 --- a/extension/trace.h +++ b/extension/trace.h @@ -158,7 +158,13 @@ static zend_always_inline int begin_profiling(zend_string *root_symbol, zend_exe } else { #if PHP_VERSION_ID >= 80000 hp_entry_t *cur_entry = hp_fast_alloc_hprof_entry(); - (cur_entry)->name_hprof = zend_string_copy((*(entries))->name_hprof); + /* Check if entries is not NULL before dereferencing */ + if (*(entries) != NULL) { + (cur_entry)->name_hprof = zend_string_copy((*(entries))->name_hprof); + } else { + /* If this is the first call and it's ignored, use the function name */ + (cur_entry)->name_hprof = zend_string_copy(function_name); + } (cur_entry)->prev_hprof = (*(entries)); (cur_entry)->is_trace = 0; (*(entries)) = (cur_entry); From c89998fd4ad4d307bd2b8266034716c44519f849 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 16:37:47 +0300 Subject: [PATCH 7/8] Prevent double-free by nullifying name_hprof after release Sets p->name_hprof to NULL after releasing the string in hp_fast_free_hprof_entry() to prevent potential double-free errors if the same entry is processed multiple times or reused from the free list with stale pointer data --- extension/trace.h | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/trace.h b/extension/trace.h index 7e1fd124..dc275057 100644 --- a/extension/trace.h +++ b/extension/trace.h @@ -111,6 +111,7 @@ static zend_always_inline void hp_fast_free_hprof_entry(hp_entry_t *p) { if (p->name_hprof != NULL) { zend_string_release(p->name_hprof); + p->name_hprof = NULL; /* Prevent double-free if entry is reused */ } /* we use/overload the prev_hprof field in the structure to link entries in From df9aace93bcdc507cc4ca3f7f0b7680c49d3b19c Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Sat, 28 Jun 2025 21:24:13 +0300 Subject: [PATCH 8/8] Revert "Prevent hp_clean_profiler_state from clearing already freed memory" This reverts commit 5a6c4e6e36083c1c6623b48ca334318ea874114b. --- extension/xhprof.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/xhprof.c b/extension/xhprof.c index 60a86705..b9f8899d 100755 --- a/extension/xhprof.c +++ b/extension/xhprof.c @@ -184,7 +184,7 @@ PHP_FUNCTION(xhprof_disable) { if (XHPROF_G(enabled)) { hp_stop(); - RETURN_ZVAL(&XHPROF_G(stats_count), 1, 1); + RETURN_ZVAL(&XHPROF_G(stats_count), 1, 0); } /* else null is returned */ } @@ -214,7 +214,7 @@ PHP_FUNCTION(xhprof_sample_disable) { if (XHPROF_G(enabled)) { hp_stop(); - RETURN_ZVAL(&XHPROF_G(stats_count), 1, 1); + RETURN_ZVAL(&XHPROF_G(stats_count), 1, 0); } /* else null is returned */ }