From 16fc9db1ef51173e9bd0dd641a1ddd27cb1e85a8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 4 Oct 2026 16:18:22 +0200 Subject: [PATCH 1/2] Fix skipped catch block when switching fibers during exception unwinding zend_fiber_object_destroy() and gc_call_destructors_in_fiber() put the pending exception aside while running another fiber, but not EG(opline_before_exception), which that fiber overwrites. When the frame handling the exception is already at ZEND_HANDLE_EXCEPTION, e.g. because a destructor ran earlier during i_free_compiled_variables(), the later zend_rethrow_exception() does not set it again, and the exception is dispatched from the wrong op, skipping the enclosing try block. --- ...troy-suspended-fiber-during-unwinding.phpt | 95 +++++++++++++++++++ Zend/tests/fibers/destructors_012.phpt | 63 ++++++++++++ Zend/zend_fibers.c | 3 + Zend/zend_gc.c | 2 + 4 files changed, 163 insertions(+) create mode 100644 Zend/tests/fibers/destroy-suspended-fiber-during-unwinding.phpt create mode 100644 Zend/tests/fibers/destructors_012.phpt diff --git a/Zend/tests/fibers/destroy-suspended-fiber-during-unwinding.phpt b/Zend/tests/fibers/destroy-suspended-fiber-during-unwinding.phpt new file mode 100644 index 000000000000..617e9d20ac73 --- /dev/null +++ b/Zend/tests/fibers/destroy-suspended-fiber-during-unwinding.phpt @@ -0,0 +1,95 @@ +--TEST-- +Destroying a suspended fiber during exception unwinding does not skip the enclosing catch block +--FILE-- +start(); + + return $fiber; +} + +function destructor_first() { + $d = new D(); + $fiber = suspended_fiber(); + throw new Exception('destructor_first'); +} + +function fiber_first() { + $fiber = suspended_fiber(); + $d = new D(); + throw new Exception('fiber_first'); +} + +function throw_in_fiber_finally() { + $d = new D(); + $fiber = new Fiber(static function () { + try { + Fiber::suspend(); + } finally { + throw new Exception('fiber finally'); + } + }); + $fiber->start(); + throw new Exception('throw_in_fiber_finally'); +} + +function catches(callable $f) { + try { + $f(); + } catch (Exception $e) { + echo 'caught: ', $e->getMessage(), $e->getPrevious() ? ', previous: '.$e->getPrevious()->getMessage() : '', "\n"; + } +} + +function catches_after_finally(callable $f) { + try { + try { + $f(); + } finally { + echo "finally\n"; + } + } catch (Exception $e) { + echo 'caught: ', $e->getMessage(), "\n"; + } +} + +foreach (['destructor_first', 'fiber_first', 'throw_in_fiber_finally'] as $f) { + echo "-- $f\n"; + try { + catches($f); + catches_after_finally($f); + } catch (Throwable $e) { + echo 'escaped: ', $e->getMessage(), "\n"; + } +} + +?> +--EXPECT-- +-- destructor_first +D::__destruct +caught: destructor_first +D::__destruct +finally +caught: destructor_first +-- fiber_first +D::__destruct +caught: fiber_first +D::__destruct +finally +caught: fiber_first +-- throw_in_fiber_finally +D::__destruct +caught: fiber finally, previous: throw_in_fiber_finally +D::__destruct +finally +caught: fiber finally diff --git a/Zend/tests/fibers/destructors_012.phpt b/Zend/tests/fibers/destructors_012.phpt new file mode 100644 index 000000000000..4a095e0e3ded --- /dev/null +++ b/Zend/tests/fibers/destructors_012.phpt @@ -0,0 +1,63 @@ +--TEST-- +Fibers in destructors 012: GC destructor fiber running during exception unwinding does not skip the enclosing catch block +--INI-- +zend.enable_gc=1 +--FILE-- +self = $this; + } + public function __destruct() { + try { + throw new Exception('Cycle::__destruct'); + } catch (Exception) { + echo "Cycle::__destruct\n"; + } + } +} + +$objects = []; +for ($i = 0; $i < 50000; $i++) { + $objects[] = new stdClass(); +} + +function f() { + global $objects; + $d = new D(); + // Releasing these copies during unwinding fills the GC root buffer + $copies = [...$objects]; + new Cycle(); + throw new Exception('f'); +} + +function c() { + try { + f(); + } catch (Exception $e) { + echo 'caught: ', $e->getMessage(), "\n"; + } +} + +$fiber = new Fiber(function () { + try { + c(); + } catch (Throwable $e) { + echo 'escaped: ', $e->getMessage(), "\n"; + } +}); +$fiber->start(); + +?> +--EXPECT-- +D::__destruct +Cycle::__destruct +caught: f diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index d571a622e476..e5219a69a3e5 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -775,6 +775,7 @@ static void zend_fiber_object_destroy(zend_object *object) } zend_object *exception = EG(exception); + const zend_op *opline_before_exception = EG(opline_before_exception); EG(exception) = NULL; zval graceful_exit; @@ -784,6 +785,8 @@ static void zend_fiber_object_destroy(zend_object *object) zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, &graceful_exit, true); + EG(opline_before_exception) = opline_before_exception; + zval_ptr_dtor(&graceful_exit); if (transfer.flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) { diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index fbe46a435fdb..eb54ead7b110 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1897,6 +1897,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) GC_G(dtor_end) = GC_G(first_unused); zend_object *exception = NULL; + const zend_op *opline_before_exception = EG(opline_before_exception); remember_prev_exception(&exception); zend_object *old_exception = exception; @@ -1933,6 +1934,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) } EG(exception) = exception; + EG(opline_before_exception) = opline_before_exception; /* Destructors ran in another fiber, out of reach of zend_call_function()'s rethrow */ if (exception && !old_exception && EG(current_execute_data) && EG(current_execute_data)->func From 48f4c0e8e1817d9469ef8e30e589dee36b59fda0 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 12 Oct 2026 16:44:31 +0200 Subject: [PATCH 2/2] [ci skip] NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index f4f2cc3b8359..f54a52230b42 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,8 @@ PHP NEWS . Fixed bug GH-24218 (Invalid opcode for count() of a literal array without SCCP). (lazerg) . Fix throwing behaviour with recursive arrays in VM. (ndossche) + . Fixed skipped catch block when switching fibers during exception unwinding + (Nicolas Grekas) - DOM: . Fixed bug GH-23352 (UAF reading an attribute value node retained across