[php-src] master: Merge branch 'PHP-8.6'
| From: | Arnaud Le Blanc | Date: | Mon, 05 Oct 2026 16:52:23 +0000 |
| Subject: | [php-src] master: Merge branch 'PHP-8.6' | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141969@lists.php.net to get a copy of this message | ||
Author: Arnaud Le Blanc (arnaud-lb)
Date: 2026-10-05T18:50:33+02:00
Commit: https://github.com/php/php-src/commit/ec2353c6b627435e57444e56aca2d5e861caee47
Raw diff: https://github.com/php/php-src/commit/ec2353c6b627435e57444e56aca2d5e861caee47.diff
Merge branch 'PHP-8.6'
* PHP-8.6:
[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 Zend/zend_gc.c
Diff:
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 6372ed0550c8..cc2e092fa05e 100644
--- a/Zend/zend_gc.c
+++ b/Zend/zend_gc.c
@@ -1957,6 +1957,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(void)
zend_object *exception = NULL;
remember_prev_exception(&exception);
+ zend_object *old_exception = exception;
if (UNEXPECTED(!fiber)) {
fiber = gc_create_destructor_fiber();
@@ -1991,6 +1992,12 @@ static zend_never_inline void gc_call_destructors_in_fiber(void)
}
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. */