[php-src] master: Merge branch 'PHP-8.6'

From: 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. */

« previous php.cvs (#141969) next »