From 61e67470aab11f6b807390ff7c4173427c452eb6 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 28 Oct 2026 04:11:25 +0000 Subject: [PATCH] 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. --- Zend/tests/gh24989.phpt | 29 +++++++++++++++++++++++++++++ Zend/tests/gh24989_uncaught.phpt | 16 ++++++++++++++++ Zend/zend_execute.c | 10 ++++++++-- 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/gh24989.phpt create mode 100644 Zend/tests/gh24989_uncaught.phpt diff --git a/Zend/tests/gh24989.phpt b/Zend/tests/gh24989.phpt new file mode 100644 index 000000000000..02b12b3e70ae --- /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-- +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 000000000000..5d3d317e806e --- /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-- + +--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 ae0b337ed2a6..45c8b8fe2823 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;