From 07eca44b4efb35685e4c0653fc5369281f65259e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 11 Jul 2026 11:33:30 -0400 Subject: [PATCH] Fix use-after-free when a destructor bails out during zend_hash_clean() zend_hash_clean() runs the element destructors and resets the table only after the loop, so a zend_bailout (memory_limit, timeout, E_USER_ERROR) from a destructor mid-walk abandons the walk with already-destroyed slots still present in the still-reachable table, and the later teardown destroys them a second time. Move each element to a local and set the slot to IS_UNDEF before running its destructor, so a bailed-out clean leaves consumed slots that the teardown skips. The buffer and packed/map shape are kept, so callers that clean during iteration are unaffected. --- NEWS | 4 +++ Zend/zend_hash.c | 24 +++++++++++++----- ext/session/tests/hash_clean_bailout.phpt | 31 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 ext/session/tests/hash_clean_bailout.phpt diff --git a/NEWS b/NEWS index 7324e5c591aa..272247f676dc 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with INT_MAX year). (arshidkv12) +- Core: + . Fixed use-after-free when a destructor bails out (memory_limit, timeout) + during zend_hash_clean(), e.g. from session_unset(). (iliaal) + - Date: . Update timelib to 2022.17. (Derick) . Fixed bug GH-19803 (Parsing a string with a single white space does create diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 23637b94bceb..ae228529284b 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -1887,12 +1887,16 @@ ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht) if (HT_HAS_STATIC_KEYS_ONLY(ht)) { if (HT_IS_WITHOUT_HOLES(ht)) { do { - ht->pDestructor(zv); + zval val = *zv; + ZVAL_UNDEF(zv); + ht->pDestructor(&val); } while (++zv != end); } else { do { if (EXPECTED(Z_TYPE_P(zv) != IS_UNDEF)) { - ht->pDestructor(zv); + zval val = *zv; + ZVAL_UNDEF(zv); + ht->pDestructor(&val); } } while (++zv != end); } @@ -1906,29 +1910,37 @@ ZEND_API void ZEND_FASTCALL zend_hash_clean(HashTable *ht) if (HT_HAS_STATIC_KEYS_ONLY(ht)) { if (HT_IS_WITHOUT_HOLES(ht)) { do { - ht->pDestructor(&p->val); + zval val = p->val; + ZVAL_UNDEF(&p->val); + ht->pDestructor(&val); } while (++p != end); } else { do { if (EXPECTED(Z_TYPE(p->val) != IS_UNDEF)) { - ht->pDestructor(&p->val); + zval val = p->val; + ZVAL_UNDEF(&p->val); + ht->pDestructor(&val); } } while (++p != end); } } else if (HT_IS_WITHOUT_HOLES(ht)) { do { - ht->pDestructor(&p->val); + zval val = p->val; + ZVAL_UNDEF(&p->val); if (EXPECTED(p->key)) { zend_string_release(p->key); } + ht->pDestructor(&val); } while (++p != end); } else { do { if (EXPECTED(Z_TYPE(p->val) != IS_UNDEF)) { - ht->pDestructor(&p->val); + zval val = p->val; + ZVAL_UNDEF(&p->val); if (EXPECTED(p->key)) { zend_string_release(p->key); } + ht->pDestructor(&val); } } while (++p != end); } diff --git a/ext/session/tests/hash_clean_bailout.phpt b/ext/session/tests/hash_clean_bailout.phpt new file mode 100644 index 000000000000..cee52a4c99e3 --- /dev/null +++ b/ext/session/tests/hash_clean_bailout.phpt @@ -0,0 +1,31 @@ +--TEST-- +Use-after-free when a destructor bails out during zend_hash_clean() via session_unset() +--EXTENSIONS-- +session +--XLEAK-- +The intentional E_USER_ERROR bailout abandons the aborted request's op_array. +--SKIPIF-- + +--INI-- +session.save_handler=files +session.use_cookies=0 +session.cache_limiter= +--FILE-- + +--EXPECTF-- +Fatal error: boom in %s on line %d