From def9d3c35315bf5dcdab34ce4282514ca8331400 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 22 Mar 2026 20:11:44 -0400 Subject: [PATCH] Fix GH-15869: Stack overflow in zend_array_destroy with deeply nested arrays zend_array_destroy() recurses through i_zval_ptr_dtor for each element, which overflows the C stack when arrays are nested deeply enough (~40-50k levels on a typical 8MB stack). Apply a tail-call optimization: when an element is an array whose refcount reaches zero, defer its destruction instead of recursing. After freeing the current hash table, loop back to destroy the deferred child. This eliminates recursion entirely for linear chains (the common crash scenario) while arrays with multiple nested children still recurse per-branch, each independently benefiting from the same optimization. Closes GH-15869 --- Zend/tests/gh15869.phpt | 17 +++++++++++++++++ Zend/zend_hash.c | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/gh15869.phpt diff --git a/Zend/tests/gh15869.phpt b/Zend/tests/gh15869.phpt new file mode 100644 index 0000000000000..46ebfd92bae76 --- /dev/null +++ b/Zend/tests/gh15869.phpt @@ -0,0 +1,17 @@ +--TEST-- +GH-15869 (Stack overflow in zend_array_destroy when freeing deeply nested arrays) +--FILE-- + +--EXPECT-- +Built +Freed diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 959becb574736..57ab64338454a 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -1820,6 +1820,11 @@ ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht) ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) { + zend_array *child; + +tail_call: + child = NULL; + IS_CONSISTENT(ht); HT_ASSERT(ht, GC_REFCOUNT(ht) <= 1); @@ -1836,12 +1841,30 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) SET_INCONSISTENT(HT_IS_DESTROYING); + /* Deferred dtor: when an element is an array with refcount reaching + * zero, save it for tail-call destruction instead of recursing. + * Prevents stack overflow with deeply nested arrays. */ +#define ZVAL_DTOR_DEFERRED(zv) do { \ + if (Z_REFCOUNTED_P(zv)) { \ + zend_refcounted *ref = Z_COUNTED_P(zv); \ + if (!GC_DELREF(ref)) { \ + if (!child && GC_TYPE(ref) == IS_ARRAY) { \ + child = (zend_array *)ref; \ + } else { \ + rc_dtor_func(ref); \ + } \ + } else { \ + gc_check_possible_root(ref); \ + } \ + } \ +} while (0) + if (HT_IS_PACKED(ht)) { zval *zv = ht->arPacked; zval *end = zv + ht->nNumUsed; do { - i_zval_ptr_dtor(zv); + ZVAL_DTOR_DEFERRED(zv); } while (++zv != end); } else { Bucket *p = ht->arData; @@ -1849,11 +1872,11 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) if (HT_HAS_STATIC_KEYS_ONLY(ht)) { do { - i_zval_ptr_dtor(&p->val); + ZVAL_DTOR_DEFERRED(&p->val); } while (++p != end); } else if (HT_IS_WITHOUT_HOLES(ht)) { do { - i_zval_ptr_dtor(&p->val); + ZVAL_DTOR_DEFERRED(&p->val); if (EXPECTED(p->key)) { zend_string_release_ex(p->key, 0); } @@ -1861,7 +1884,7 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) } else { do { if (EXPECTED(Z_TYPE(p->val) != IS_UNDEF)) { - i_zval_ptr_dtor(&p->val); + ZVAL_DTOR_DEFERRED(&p->val); if (EXPECTED(p->key)) { zend_string_release_ex(p->key, 0); } @@ -1869,6 +1892,7 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) } while (++p != end); } } +#undef ZVAL_DTOR_DEFERRED } else if (EXPECTED(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) { goto free_ht; } @@ -1877,6 +1901,11 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht) free_ht: zend_hash_iterators_remove(ht); FREE_HASHTABLE(ht); + + if (UNEXPECTED(child)) { + ht = (HashTable *)child; + goto tail_call; + } } ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht)