[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
| From: | Arnaud Le Blanc | Date: | Mon, 05 Oct 2026 16:52:21 +0000 |
| Subject: | [php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5 | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141968@lists.php.net to get a copy of this message | ||
Author: Arnaud Le Blanc (arnaud-lb)
Date: 2026-10-05T18:49:59+02:00
Commit: https://github.com/php/php-src/commit/f2a89256996908816955c573968715000c3426af
Raw diff: https://github.com/php/php-src/commit/f2a89256996908816955c573968715000c3426af.diff
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
[ci skip] NEWS
Rethrow exceptions from destructors called by the GC in a fiber (#24118)
Changed paths:
A Zend/tests/fibers/destructors_013.phpt
A Zend/tests/fibers/destructors_014.phpt
A Zend/tests/fibers/destructors_015.phpt
M NEWS
M Zend/zend_gc.c
Diff:
diff --git a/NEWS b/NEWS
index 4d949ee74209..94cad245e0f1 100644
--- a/NEWS
+++ b/NEWS
@@ -22,6 +22,8 @@ PHP NEWS
function and a later request calls it. (Ilia Alshanetsky)
. Fixed System V shared memory emulation for Windows attaching segments past
the end of their mapping. (Ilia Alshanetsky)
+ . Fixed exception thrown by destructor during GC in a Fiber not being rethrown
+ into the frame that triggered the GC. (Nicolas Grekas)
- CLI
. Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during
diff --git a/Zend/tests/fibers/destructors_013.phpt b/Zend/tests/fibers/destructors_013.phpt
new file mode 100644
index 000000000000..0e555b370569
--- /dev/null
+++ b/Zend/tests/fibers/destructors_013.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Fibers in destructors 013: Exception thrown by a destructor called by the GC is thrown by the
statement that triggered the GC
+--INI--
+zend.enable_gc=1
+--FILE--
+<?php
+
+class Cycle {
+ public $self;
+ public function __construct() {
+ $this->self = $this;
+ }
+ public function __destruct() {
+ echo "Cycle::__destruct\n";
+ throw new Exception('Cycle::__destruct');
+ }
+}
+
+$objects = [];
+for ($i = 0; $i < 20000; $i++) {
+ $objects[] = new stdClass();
+}
+
+function f() {
+ global $objects;
+ try {
+ $copies = [...$objects];
+ new Cycle();
+ // Releasing the copies fills the GC root buffer
+ $copies = null;
+ echo "Not reached\n";
+ } catch (Exception $e) {
+ echo 'Caught: ', $e->getMessage(), "\n";
+ }
+}
+
+$fiber = new Fiber(function () {
+ try {
+ f();
+ } catch (Exception $e) {
+ echo 'Escaped: ', $e->getMessage(), "\n";
+ }
+});
+$fiber->start();
+
+?>
+--EXPECT--
+Cycle::__destruct
+Caught: Cycle::__destruct
diff --git a/Zend/tests/fibers/destructors_014.phpt b/Zend/tests/fibers/destructors_014.phpt
new file mode 100644
index 000000000000..be8c335e6781
--- /dev/null
+++ b/Zend/tests/fibers/destructors_014.phpt
@@ -0,0 +1,55 @@
+--TEST--
+Fibers in destructors 014: Exception thrown by a destructor called by the GC during exception
unwinding is chained
+--INI--
+zend.enable_gc=1
+--FILE--
+<?php
+
+class Cycle {
+ public $self;
+ public function __construct() {
+ $this->self = $this;
+ }
+ public function __destruct() {
+ echo "Cycle::__destruct\n";
+ throw new Exception('Cycle::__destruct');
+ }
+}
+
+$objects = [];
+for ($i = 0; $i < 20000; $i++) {
+ $objects[] = new stdClass();
+}
+
+function f() {
+ global $objects;
+ // Releasing these copies during unwinding fills the GC root buffer
+ $copies = [...$objects];
+ new Cycle();
+ throw new Exception('f');
+}
+
+function g() {
+ try {
+ f();
+ echo "Not reached\n";
+ } catch (Exception $e) {
+ echo 'Caught: ', $e->getMessage(), "\n";
+ echo 'Previous: ', $e->getPrevious()->getMessage(), "\n";
+ }
+}
+
+$fiber = new Fiber(function () {
+ try {
+ g();
+ } catch (Exception $e) {
+ echo 'Escaped: ', $e->getMessage(), "\n";
+ }
+});
+$fiber->start();
+
+?>
+--EXPECT--
+Cycle::__destruct
+Caught: Cycle::__destruct
+Previous: f
diff --git a/Zend/tests/fibers/destructors_015.phpt b/Zend/tests/fibers/destructors_015.phpt
new file mode 100644
index 000000000000..481c69fc795a
--- /dev/null
+++ b/Zend/tests/fibers/destructors_015.phpt
@@ -0,0 +1,60 @@
+--TEST--
+Fibers in destructors 015: Exception thrown by a destructor called by the GC from internal code
+--INI--
+zend.enable_gc=1
+--FILE--
+<?php
+
+class Cycle {
+ public $self;
+ public function __construct() {
+ $this->self = $this;
+ }
+ public function __destruct() {
+ echo "Cycle::__destruct\n";
+ throw new Exception('Cycle::__destruct');
+ }
+}
+
+$fiber = new Fiber(function () {
+ try {
+ new Cycle();
+ gc_collect_cycles();
+ echo "Not reached\n";
+ } catch (Exception $e) {
+ echo 'Caught: ', $e->getMessage(), "\n";
+ }
+});
+$fiber->start();
+
+$objects = [];
+for ($i = 0; $i < 20000; $i++) {
+ $objects[] = new stdClass();
+}
+
+function create_fiber() {
+ global $objects;
+ $copies = [...$objects];
+
+ // The fiber releases the copies after its function returned, which fills the GC root buffer
+ return new Fiber(function () use ($copies) {
+ new Cycle();
+ echo "Return\n";
+ });
+}
+
+$fiber = create_fiber();
+try {
+ $fiber->start();
+ echo "Not reached\n";
+} catch (Exception $e) {
+ echo 'Caught: ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Cycle::__destruct
+Caught: Cycle::__destruct
+Return
+Cycle::__destruct
+Caught: Cycle::__destruct
diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c
index 066453552ba8..281994dbfd7f 100644
--- a/Zend/zend_gc.c
+++ b/Zend/zend_gc.c
@@ -1958,6 +1958,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
zend_object *exception = NULL;
remember_prev_exception(&exception);
+ zend_object *old_exception = exception;
if (UNEXPECTED(!fiber)) {
fiber = gc_create_destructor_fiber();
@@ -1992,6 +1993,12 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
}
EG(exception) = 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
+ && ZEND_USER_CODE(EG(current_execute_data)->func->common.type)) {
+ zend_rethrow_exception(EG(current_execute_data));
+ }
}
/* Perform a garbage collection run. The default implementation of gc_collect_cycles. */