Skip to content

Commit 64b6450

Browse files
committed
Use destroy stack for multi-branch deeply nested arrays
The single-child tail-call only defers one array per level. When multiple children all reach refcount zero (e.g. two independent deep chains in one array), the extras still recursed through rc_dtor_func and could overflow the C stack. Replace the fallback rc_dtor_func call for additional array children with a heap-backed destroy stack (small on-stack buffer for the common case, grows via emalloc when needed). Linear chains still use the zero-allocation tail-call path.
1 parent def9d3c commit 64b6450

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed
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: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,6 +1821,13 @@ ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)
18211821
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18221822
{
18231823
zend_array *child;
1824+
/* On-stack buffer avoids heap allocation for the common case. Arrays
1825+
* with multiple nested children whose refcount all reach zero push
1826+
* extras here instead of recursing through rc_dtor_func. */
1827+
zend_array *ds_buf[8];
1828+
zend_array **ds = ds_buf;
1829+
size_t ds_size = 0;
1830+
size_t ds_cap = sizeof(ds_buf) / sizeof(ds_buf[0]);
18241831

18251832
tail_call:
18261833
child = NULL;
@@ -1841,15 +1848,31 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18411848

18421849
SET_INCONSISTENT(HT_IS_DESTROYING);
18431850

1844-
/* Deferred dtor: when an element is an array with refcount reaching
1845-
* zero, save it for tail-call destruction instead of recursing.
1846-
* Prevents stack overflow with deeply nested arrays. */
1851+
/* When an element is an array whose refcount reaches zero, defer its
1852+
* destruction instead of recursing. The first child is kept for a
1853+
* tail-call (zero overhead for linear chains). Additional children
1854+
* are pushed onto a destroy stack, eliminating C stack growth
1855+
* regardless of nesting shape. */
18471856
#define ZVAL_DTOR_DEFERRED(zv) do { \
18481857
if (Z_REFCOUNTED_P(zv)) { \
18491858
zend_refcounted *ref = Z_COUNTED_P(zv); \
18501859
if (!GC_DELREF(ref)) { \
1851-
if (!child && GC_TYPE(ref) == IS_ARRAY) { \
1852-
child = (zend_array *)ref; \
1860+
if (GC_TYPE(ref) == IS_ARRAY) { \
1861+
if (!child) { \
1862+
child = (zend_array *)ref; \
1863+
} else { \
1864+
if (UNEXPECTED(ds_size >= ds_cap)) { \
1865+
if (ds == ds_buf) { \
1866+
ds_cap = 32; \
1867+
ds = emalloc(ds_cap * sizeof(zend_array *)); \
1868+
memcpy(ds, ds_buf, sizeof(ds_buf)); \
1869+
} else { \
1870+
ds_cap *= 2; \
1871+
ds = erealloc(ds, ds_cap * sizeof(zend_array *)); \
1872+
} \
1873+
} \
1874+
ds[ds_size++] = (zend_array *)ref; \
1875+
} \
18531876
} else { \
18541877
rc_dtor_func(ref); \
18551878
} \
@@ -1906,6 +1929,15 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
19061929
ht = (HashTable *)child;
19071930
goto tail_call;
19081931
}
1932+
1933+
if (UNEXPECTED(ds_size > 0)) {
1934+
ht = (HashTable *)ds[--ds_size];
1935+
goto tail_call;
1936+
}
1937+
1938+
if (UNEXPECTED(ds != ds_buf)) {
1939+
efree(ds);
1940+
}
19091941
}
19101942

19111943
ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht)

0 commit comments

Comments
 (0)