Skip to content

Fix skipped catch block when switching fibers during exception unwinding - #24117

Merged
arnaud-lb merged 3 commits into
php:PHP-8.4from
nicolas-grekas:fiber-destroy-opline
Oct 12, 2026
Merged

arnaud-lb merged 3 commits into
php:PHP-8.4from
nicolas-grekas:fiber-destroy-opline

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Destroying a suspended fiber while an exception unwinds can make the engine skip the catch block that should handle that exception:

<?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).

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.
* PHP-8.4: (33 commits)
  Update NEWS
  ext/mbstring: Fix truncated encoding names resolving to full names (php#23974)
  Backport PCRE2 security fixes to bundled 10.40
  Fix backport
  ext/pdo: Avoid a duplicate warning for a rejected statement class
  phpdbg: Fix wrong command names
  Backport phpGH-24241 to 8.4
  [ci skip] Update EXTENSIONS and CODEOWNERS for ext/readline
  Fix throwing behaviour with recursive arrays in VM
  Fix phpGH-24229: Use SHUT_WR when closing FastCGI connections (php#24230)
  Add regression test for phpGH-16821 (php#24228)
  Fix use-after-free when removing a failing zlib.inflate filter (php#24176)
  Fix phpGH-24218: Invalid opcode for count() of a literal array without SCCP
  fix: system ID for Apache graceful restart (php#24184)
  ext/standard: Validate the bcrypt cost before reading it
  zend_alloc: move a small block shrunk to the size of the bin below
  Fix phpGH-23979: Nullsafe operator must not flush delayed oplines of an enclosing function
  ext/zip: Reject ZipArchive mutators during close() (php#24025)
  Fix OSS-Fuzz #568005340: FETCH_DIM_FUNC_ARG partial conversion
  Fix too wide type inference for ASSIGN_DIM_OP
  ...
@arnaud-lb
arnaud-lb merged commit 785f714 into php:PHP-8.4 Oct 12, 2026
1 check passed
arnaud-lb added a commit that referenced this pull request Oct 12, 2026
* PHP-8.6:
  Fix skipped catch block when switching fibers during exception unwinding (#24117)
@arnaud-lb

Copy link
Copy Markdown
Member

Thank you!

narfbg pushed a commit to narfbg/php-src that referenced this pull request Oct 12, 2026
* PHP-8.4:
  Fix skipped catch block when switching fibers during exception unwinding (php#24117)
narfbg pushed a commit to narfbg/php-src that referenced this pull request Oct 12, 2026
* PHP-8.5:
  Fix skipped catch block when switching fibers during exception unwinding (php#24117)
@nicolas-grekas
nicolas-grekas deleted the fiber-destroy-opline branch October 12, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants