Skip to content

Commit 16fc9db

Browse files
Fix skipped catch block when switching fibers during exception unwinding
zend_fiber_object_destroy() and gc_call_destructors_in_fiber() put the pending exception aside while running another fiber, but not EG(opline_before_exception), which that fiber overwrites. When the frame handling the exception is already at ZEND_HANDLE_EXCEPTION, e.g. because a destructor ran earlier during i_free_compiled_variables(), the later zend_rethrow_exception() does not set it again, and the exception is dispatched from the wrong op, skipping the enclosing try block.
1 parent bba11e6 commit 16fc9db

4 files changed

Lines changed: 163 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
--TEST--
2+
Destroying a suspended fiber during exception unwinding does not skip the enclosing catch block
3+
--FILE--
4+
<?php
5+
6+
class D {
7+
public function __destruct() {
8+
echo "D::__destruct\n";
9+
}
10+
}
11+
12+
function suspended_fiber(): Fiber {
13+
$fiber = new Fiber(static function () {
14+
Fiber::suspend();
15+
});
16+
$fiber->start();
17+
18+
return $fiber;
19+
}
20+
21+
function destructor_first() {
22+
$d = new D();
23+
$fiber = suspended_fiber();
24+
throw new Exception('destructor_first');
25+
}
26+
27+
function fiber_first() {
28+
$fiber = suspended_fiber();
29+
$d = new D();
30+
throw new Exception('fiber_first');
31+
}
32+
33+
function throw_in_fiber_finally() {
34+
$d = new D();
35+
$fiber = new Fiber(static function () {
36+
try {
37+
Fiber::suspend();
38+
} finally {
39+
throw new Exception('fiber finally');
40+
}
41+
});
42+
$fiber->start();
43+
throw new Exception('throw_in_fiber_finally');
44+
}
45+
46+
function catches(callable $f) {
47+
try {
48+
$f();
49+
} catch (Exception $e) {
50+
echo 'caught: ', $e->getMessage(), $e->getPrevious() ? ', previous: '.$e->getPrevious()->getMessage() : '', "\n";
51+
}
52+
}
53+
54+
function catches_after_finally(callable $f) {
55+
try {
56+
try {
57+
$f();
58+
} finally {
59+
echo "finally\n";
60+
}
61+
} catch (Exception $e) {
62+
echo 'caught: ', $e->getMessage(), "\n";
63+
}
64+
}
65+
66+
foreach (['destructor_first', 'fiber_first', 'throw_in_fiber_finally'] as $f) {
67+
echo "-- $f\n";
68+
try {
69+
catches($f);
70+
catches_after_finally($f);
71+
} catch (Throwable $e) {
72+
echo 'escaped: ', $e->getMessage(), "\n";
73+
}
74+
}
75+
76+
?>
77+
--EXPECT--
78+
-- destructor_first
79+
D::__destruct
80+
caught: destructor_first
81+
D::__destruct
82+
finally
83+
caught: destructor_first
84+
-- fiber_first
85+
D::__destruct
86+
caught: fiber_first
87+
D::__destruct
88+
finally
89+
caught: fiber_first
90+
-- throw_in_fiber_finally
91+
D::__destruct
92+
caught: fiber finally, previous: throw_in_fiber_finally
93+
D::__destruct
94+
finally
95+
caught: fiber finally
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Fibers in destructors 012: GC destructor fiber running during exception unwinding does not skip the enclosing catch block
3+
--INI--
4+
zend.enable_gc=1
5+
--FILE--
6+
<?php
7+
8+
class D {
9+
public function __destruct() {
10+
echo "D::__destruct\n";
11+
}
12+
}
13+
14+
class Cycle {
15+
public $self;
16+
public function __construct() {
17+
$this->self = $this;
18+
}
19+
public function __destruct() {
20+
try {
21+
throw new Exception('Cycle::__destruct');
22+
} catch (Exception) {
23+
echo "Cycle::__destruct\n";
24+
}
25+
}
26+
}
27+
28+
$objects = [];
29+
for ($i = 0; $i < 50000; $i++) {
30+
$objects[] = new stdClass();
31+
}
32+
33+
function f() {
34+
global $objects;
35+
$d = new D();
36+
// Releasing these copies during unwinding fills the GC root buffer
37+
$copies = [...$objects];
38+
new Cycle();
39+
throw new Exception('f');
40+
}
41+
42+
function c() {
43+
try {
44+
f();
45+
} catch (Exception $e) {
46+
echo 'caught: ', $e->getMessage(), "\n";
47+
}
48+
}
49+
50+
$fiber = new Fiber(function () {
51+
try {
52+
c();
53+
} catch (Throwable $e) {
54+
echo 'escaped: ', $e->getMessage(), "\n";
55+
}
56+
});
57+
$fiber->start();
58+
59+
?>
60+
--EXPECT--
61+
D::__destruct
62+
Cycle::__destruct
63+
caught: f

‎Zend/zend_fibers.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@ static void zend_fiber_object_destroy(zend_object *object)
775775
}
776776

777777
zend_object *exception = EG(exception);
778+
const zend_op *opline_before_exception = EG(opline_before_exception);
778779
EG(exception) = NULL;
779780

780781
zval graceful_exit;
@@ -784,6 +785,8 @@ static void zend_fiber_object_destroy(zend_object *object)
784785

785786
zend_fiber_transfer transfer = zend_fiber_resume_internal(fiber, &graceful_exit, true);
786787

788+
EG(opline_before_exception) = opline_before_exception;
789+
787790
zval_ptr_dtor(&graceful_exit);
788791

789792
if (transfer.flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) {

‎Zend/zend_gc.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
18971897
GC_G(dtor_end) = GC_G(first_unused);
18981898

18991899
zend_object *exception = NULL;
1900+
const zend_op *opline_before_exception = EG(opline_before_exception);
19001901
remember_prev_exception(&exception);
19011902
zend_object *old_exception = exception;
19021903

@@ -1933,6 +1934,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
19331934
}
19341935

19351936
EG(exception) = exception;
1937+
EG(opline_before_exception) = opline_before_exception;
19361938

19371939
/* Destructors ran in another fiber, out of reach of zend_call_function()'s rethrow */
19381940
if (exception && !old_exception && EG(current_execute_data) && EG(current_execute_data)->func

0 commit comments

Comments
 (0)