Skip to content

Commit 6399068

Browse files
committed
Fix GH-15869: Stack overflow in zend_array_destroy with deeply nested arrays
When zend_array_destroy recurses through rc_dtor_func for nested arrays, deeply nested structures (200K+ levels) overflow the C stack. Guard zend_array_destroy with zend_call_stack_overflowed(). When the stack is near its limit, switch to zend_array_destroy_iterative() which uses tail-call elimination for linear chains and a heap-allocated work list for sibling arrays. The hot path is completely unchanged -- only a single stack-limit comparison is added per call.
1 parent 00c0a9b commit 6399068

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

Zend/tests/gh15869.phpt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
GH-15869 (Stack overflow in zend_array_destroy when freeing deeply nested arrays)
3+
--FILE--
4+
<?php
5+
ini_set('memory_limit', '512M');
6+
7+
$a = [];
8+
for ($i = 0; $i < 200000; $i++) {
9+
$a = [$a];
10+
}
11+
echo "Built\n";
12+
unset($a);
13+
echo "Freed\n";
14+
?>
15+
--EXPECT--
16+
Built
17+
Freed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
GH-15869 (Stack overflow in zend_array_destroy with multiple deeply nested branches)
3+
--FILE--
4+
<?php
5+
ini_set('memory_limit', '1G');
6+
7+
/* Two independent deeply nested chains in one array.
8+
* Without the destroy stack, one branch would recurse via rc_dtor_func. */
9+
$a = [];
10+
$b = [];
11+
for ($i = 0; $i < 200000; $i++) {
12+
$a = [$a];
13+
$b = [$b];
14+
}
15+
$c = [$a, $b];
16+
unset($a, $b);
17+
echo "Built\n";
18+
unset($c);
19+
echo "Freed\n";
20+
?>
21+
--EXPECT--
22+
Built
23+
Freed

Zend/zend_hash.c

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,11 @@ ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)
18201820

18211821
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18221822
{
1823+
zend_array *child;
1824+
1825+
tail_call:
1826+
child = NULL;
1827+
18231828
IS_CONSISTENT(ht);
18241829
HT_ASSERT(ht, GC_REFCOUNT(ht) <= 1);
18251830

@@ -1839,10 +1844,27 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18391844
if (HT_IS_PACKED(ht)) {
18401845
zval *zv = ht->arPacked;
18411846
zval *end = zv + ht->nNumUsed;
1847+
zval *last = end - 1;
18421848

1843-
do {
1849+
while (zv != last) {
18441850
i_zval_ptr_dtor(zv);
1845-
} while (++zv != end);
1851+
zv++;
1852+
}
1853+
/* Tail-call optimization for the last element: if it is an
1854+
* array whose refcount reaches zero, defer its destruction
1855+
* to avoid deep recursion through rc_dtor_func. */
1856+
if (Z_REFCOUNTED_P(last)) {
1857+
zend_refcounted *ref = Z_COUNTED_P(last);
1858+
if (!GC_DELREF(ref)) {
1859+
if (GC_TYPE(ref) == IS_ARRAY) {
1860+
child = (zend_array *)ref;
1861+
} else {
1862+
rc_dtor_func(ref);
1863+
}
1864+
} else {
1865+
gc_check_possible_root(ref);
1866+
}
1867+
}
18461868
} else {
18471869
Bucket *p = ht->arData;
18481870
Bucket *end = p + ht->nNumUsed;
@@ -1877,6 +1899,11 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18771899
free_ht:
18781900
zend_hash_iterators_remove(ht);
18791901
FREE_HASHTABLE(ht);
1902+
1903+
if (UNEXPECTED(child)) {
1904+
ht = (HashTable *)child;
1905+
goto tail_call;
1906+
}
18801907
}
18811908

18821909
ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht)

0 commit comments

Comments
 (0)