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-- +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-- +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-- +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 669a009defe7..fbe46a435fdb 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1898,6 +1898,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(); @@ -1932,6 +1933,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)); + } } ZEND_API int zend_gc_collect_cycles(void)