Skip to content

Rethrow exceptions from destructors called by the GC in a fiber - #24118

Merged
arnaud-lb merged 1 commit into
php:PHP-8.4from
nicolas-grekas:gc-fiber-dtor-rethrow
Oct 5, 2026
Merged

arnaud-lb merged 1 commit into
php:PHP-8.4from
nicolas-grekas:gc-fiber-dtor-rethrow

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Found while working on #24117.

Since 8.4, when the GC is triggered inside a fiber, it calls destructors in a dedicated fiber. If one of them throws, the exception isn't rethrown into the frame that triggered the GC: the code after the triggering statement keeps running, and the exception then escapes the enclosing catch block.

class Cycle {
    public $self;
    public function __construct() { $this->self = $this; }
    public function __destruct() { throw new Exception('from __destruct'); }
}

$objects = [];
for ($i = 0; $i < 20000; $i++) { $objects[] = new stdClass(); }

function g() {
    global $objects;
    try {
        $copies = [...$objects];
        new Cycle();
        $copies = null; // fills the GC root buffer
        echo "after assign\n";
    } catch (Exception $e) {
        echo "caught in g()\n";
    }
}

(new Fiber(function () {
    try {
        g();
    } catch (Exception $e) {
        echo "escaped g()\n";
    }
}))->start();

8.3 prints caught in g(), 8.4 and up print after assign then escaped g().

zend_call_function() can't rethrow into that frame from the destructor fiber, so gc_call_destructors_in_fiber() now does it when no exception was pending before the GC run, like zend_fiber_object_destroy() does.

This conflicts with #24117 on the last lines of gc_call_destructors_in_fiber(): the EG(opline_before_exception) restore has to come before the rethrow, which destructors_013.phpt checks.

When the GC runs destructors in its dedicated fiber, zend_call_function()
cannot rethrow their exception into the frame that triggered the GC, since
that frame belongs to another fiber. gc_call_destructors_in_fiber() then
restores EG(exception) without rethrowing it, so the code following the
statement that triggered the GC keeps running until something checks
EG(exception), and the exception is dispatched from a stale
EG(opline_before_exception), which can skip the enclosing catch block.

Rethrow it into the current frame when no exception was pending before the
GC run, as zend_fiber_object_destroy() does.
@arnaud-lb
arnaud-lb merged commit 1bc7e57 into php:PHP-8.4 Oct 5, 2026
18 checks passed
arnaud-lb added a commit that referenced this pull request Oct 5, 2026
* PHP-8.6:
  [ci skip] NEWS
  Rethrow exceptions from destructors called by the GC in a fiber (#24118)
@arnaud-lb

Copy link
Copy Markdown
Member

Thank you!

arnaud-lb added a commit to nicolas-grekas/php-src that referenced this pull request Oct 5, 2026
* up/PHP-8.4:
  [ci skip] NEWS
  Rethrow exceptions from destructors called by the GC in a fiber (php#24118)
  ext/intl: Use byte offsets in IntlDateFormatter parsing
  ext/dom: Restore the XPath context after a reentrant evaluation
  Fix TLS stream EOF detection after close_notify with stale errno (php#24132)
  Re-generate outdated parse_date.c file
  ext/standard: Close owned proc_open descriptors on setup failure
  ext/standard: Keep IPTC headers local to each call
  ext/standard: Reject incomplete sha1_file reads
t0ny4 pushed a commit to t0ny4/php-src that referenced this pull request Oct 5, 2026
* PHP-8.4:
  [ci skip] NEWS
  Rethrow exceptions from destructors called by the GC in a fiber (php#24118)
t0ny4 pushed a commit to t0ny4/php-src that referenced this pull request Oct 5, 2026
* PHP-8.5:
  [ci skip] NEWS
  Rethrow exceptions from destructors called by the GC in a fiber (php#24118)
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