Skip to content

Commit 8d66718

Browse files
iliaalclaude
andcommitted
fix: use fixed-size destroy stack to avoid OOM during array cleanup
Replace the dynamically growing heap buffer (emalloc/erealloc) with a fixed-size on-stack array of 32 entries. When the buffer is full, fall back to a recursive zend_array_destroy call instead of heap allocation. This prevents Fatal OOM errors during shutdown when memory_limit is already exhausted, as seen in stack_limit_014 on x32 and macOS ARM64. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 64b6450 commit 8d66718

File tree

1 file changed

+10
-23
lines changed

1 file changed

+10
-23
lines changed

Zend/zend_hash.c

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,13 +1821,12 @@ 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;
1824+
/* Fixed-size buffer for deferred array destruction. Arrays with multiple
1825+
* nested children whose refcount all reach zero are pushed here instead
1826+
* of recursing through rc_dtor_func. If the buffer fills, we fall back
1827+
* to a recursive zend_array_destroy call for the overflow. */
1828+
zend_array *ds[32];
18291829
size_t ds_size = 0;
1830-
size_t ds_cap = sizeof(ds_buf) / sizeof(ds_buf[0]);
18311830

18321831
tail_call:
18331832
child = NULL;
@@ -1851,27 +1850,19 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
18511850
/* When an element is an array whose refcount reaches zero, defer its
18521851
* destruction instead of recursing. The first child is kept for a
18531852
* 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. */
1853+
* are pushed onto a fixed-size destroy stack. If the stack is full,
1854+
* fall back to a recursive zend_array_destroy call. */
18561855
#define ZVAL_DTOR_DEFERRED(zv) do { \
18571856
if (Z_REFCOUNTED_P(zv)) { \
18581857
zend_refcounted *ref = Z_COUNTED_P(zv); \
18591858
if (!GC_DELREF(ref)) { \
18601859
if (GC_TYPE(ref) == IS_ARRAY) { \
18611860
if (!child) { \
18621861
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-
} \
1862+
} else if (EXPECTED(ds_size < sizeof(ds) / sizeof(ds[0]))) { \
18741863
ds[ds_size++] = (zend_array *)ref; \
1864+
} else { \
1865+
zend_array_destroy((HashTable *)ref); \
18751866
} \
18761867
} else { \
18771868
rc_dtor_func(ref); \
@@ -1934,10 +1925,6 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
19341925
ht = (HashTable *)ds[--ds_size];
19351926
goto tail_call;
19361927
}
1937-
1938-
if (UNEXPECTED(ds != ds_buf)) {
1939-
efree(ds);
1940-
}
19411928
}
19421929

19431930
ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht)

0 commit comments

Comments
 (0)