Commit 7abaad3e21d for php
commit 7abaad3e21dbc5ef8dec1a1db314d54ba1803800
Author: David Carlier <devnexen@gmail.com>
Date: Wed Oct 28 04:11:25 2026 +0000
Fix GH-24989: Use-after-free in GC when an exception is thrown.
cleanup_live_vars() freed live temporaries without clearing their slots,
so GC triggered by a destructor during unwinding read freed memory.
Close GH-24993
diff --git a/NEWS b/NEWS
index f74b0b57f67..6532e398594 100644
--- a/NEWS
+++ b/NEWS
@@ -34,6 +34,8 @@ PHP NEWS
existing CV). (yasu20200121web-prog, lazerg)
. Fixed unmapping of an unrelated range when the aligned chunk mmap fails
(Marti2203).
+ . Fixed bug GH-24989 (Use-after-free in GC when an exception is thrown).
+ (David Carlier)
- CLI:
. Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
diff --git a/Zend/tests/gh24989.phpt b/Zend/tests/gh24989.phpt
new file mode 100644
index 00000000000..02b12b3e70a
--- /dev/null
+++ b/Zend/tests/gh24989.phpt
@@ -0,0 +1,29 @@
+--TEST--
+GH-24989 (Use-after-free in GC when an exception is thrown)
+--CREDITS--
+vi3tL0u1s
+--FILE--
+<?php
+class D { function __destruct() { gc_collect_cycles(); } }
+
+function loop_var() {
+ foreach (range(1, 2) as $v) {
+ $x = [new D, throw new Exception('loop')];
+ }
+}
+
+function tmp_var() {
+ $x = [range(1, 2), [new D, throw new Exception('tmp')]];
+}
+
+foreach (['loop_var', 'tmp_var'] as $f) {
+ try {
+ $f();
+ } catch (Exception $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+}
+?>
+--EXPECT--
+Exception: loop
+Exception: tmp
diff --git a/Zend/tests/gh24989_uncaught.phpt b/Zend/tests/gh24989_uncaught.phpt
new file mode 100644
index 00000000000..5d3d317e806
--- /dev/null
+++ b/Zend/tests/gh24989_uncaught.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-24989 (Use-after-free in GC when an exception is thrown, uncaught)
+--CREDITS--
+vi3tL0u1s
+--FILE--
+<?php
+class D { function __destruct() { gc_collect_cycles(); } }
+foreach (range(1, 2) as $v) {
+ $x = [new D, throw new Exception];
+}
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 {main}
+ thrown in %s on line %d
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index ae0b337ed2a..45c8b8fe282 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -4713,7 +4713,10 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num,
}
if (kind == ZEND_LIVE_TMPVAR) {
- zval_ptr_dtor_nogc(var);
+ zval tmp;
+ ZVAL_COPY_VALUE(&tmp, var);
+ ZVAL_UNDEF(var);
+ zval_ptr_dtor_nogc(&tmp);
} else if (kind == ZEND_LIVE_NEW) {
zend_object *obj;
ZEND_ASSERT(Z_TYPE_P(var) == IS_OBJECT);
@@ -4724,7 +4727,10 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num,
if (Z_TYPE_P(var) != IS_ARRAY && Z_FE_ITER_P(var) != (uint32_t)-1) {
zend_hash_iterator_del(Z_FE_ITER_P(var));
}
- zval_ptr_dtor_nogc(var);
+ zval tmp;
+ ZVAL_COPY_VALUE(&tmp, var);
+ ZVAL_UNDEF(var);
+ zval_ptr_dtor_nogc(&tmp);
} else if (kind == ZEND_LIVE_ROPE) {
zend_string **rope = (zend_string **)var;
zend_op *last = EX(func)->op_array.opcodes + op_num;