[php-src] PR #24117: Fix skipped catch block when switching fibers during exception unwinding
| From: | nicolas-grekas | Date: | Sun, 04 Oct 2026 14:25:47 +0000 |
| Subject: | [php-src] PR #24117: Fix skipped catch block when switching fibers during exception unwinding | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39129@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24117
Author: nicolas-grekas
Destroying a suspended fiber while an exception unwinds can make the engine skip the catch block
that should handle that exception:
```php
<?php
class D { public function __destruct() {} }
function f() {
$d = new D();
$fiber = new Fiber(static function () { Fiber::suspend(); });
$fiber->start();
throw new Exception('thrown in f()');
}
function c() {
try {
f();
} catch (Exception $e) {
echo "caught in c()\n";
}
}
c(); // Fatal error: Uncaught Exception: thrown in f()
```
zend_fiber_object_destroy() puts the pending exception aside while it resumes the
fiber, but not EG(opline_before_exception), which the fiber overwrites. Because
D::__destruct() ran first, the frame of c() is already at
ZEND_HANDLE_EXCEPTION, so the zend_rethrow_exception() that follows
i_free_compiled_variables() doesn't set it again and the exception is dispatched
from the wrong op. gc_call_destructors_in_fiber() has the same issue when the GC runs
during unwinding inside a fiber (8.4+).
The fix saves and restores the pointer at both places, like
zend_objects_destroy_object() and zend_generator_dtor_storage() already do
(see GH-20183 and GH-20714 for the generator side). Saving it in zend_fiber_vm_state
instead would also work, but backtraces taken in a GC destructor would then report a wrong line for
the unwinding frames.
Found while investigating a random failure on Symfony's CI, where an exception escaped
Worker::run() when the GC destroyed suspended amphp fibers during unwinding
(symfony/symfony#66615).