Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)

@LamentXU123 LamentXU123 Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior difference is shown in ext/session right (given your test)? So I think there should be an entry in the session extension or even shouldn't be any entry.

Because AFAIK the NEWS file is for the outsiders (comparing to UPGRADING.INTERNALS). php devs have no idea what zend_hash_clean is.

Just my 2 cents, I am not more familiar with the policy than you :)

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
Expand Down
24 changes: 18 additions & 6 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
31 changes: 31 additions & 0 deletions ext/session/tests/hash_clean_bailout.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php include('skipif.inc'); ?>
--INI--
session.save_handler=files
session.use_cookies=0
session.cache_limiter=
--FILE--
<?php
error_reporting(E_ALL & ~E_DEPRECATED);
class Dtor {
public function __destruct() {}
}
class Killer {
public function __destruct() {
trigger_error("boom", E_USER_ERROR);
}
}
session_start();
$_SESSION['a'] = new Dtor();
$_SESSION['b'] = new Killer();
$_SESSION['c'] = new Dtor();
session_unset();
?>
--EXPECTF--
Fatal error: boom in %s on line %d
Loading