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;