Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ PHP NEWS
. Fixed bug GH-24218 (Invalid opcode for count() of a literal array
without SCCP). (lazerg)
. Fix throwing behaviour with recursive arrays in VM. (ndossche)
. Fixed skipped catch block when switching fibers during exception unwinding
(Nicolas Grekas)

- DOM:
. Fixed bug GH-23352 (UAF reading an attribute value node retained across
Expand Down
95 changes: 95 additions & 0 deletions Zend/tests/fibers/destroy-suspended-fiber-during-unwinding.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
--TEST--
Destroying a suspended fiber during exception unwinding does not skip the enclosing catch block
--FILE--
<?php

class D {
public function __destruct() {
echo "D::__destruct\n";
}
}

function suspended_fiber(): Fiber {
$fiber = new Fiber(static function () {
Fiber::suspend();
});
$fiber->start();

return $fiber;
}

function destructor_first() {
$d = new D();
$fiber = suspended_fiber();
throw new Exception('destructor_first');
}

function fiber_first() {
$fiber = suspended_fiber();
$d = new D();
throw new Exception('fiber_first');
}

function throw_in_fiber_finally() {
$d = new D();
$fiber = new Fiber(static function () {
try {
Fiber::suspend();
} finally {
throw new Exception('fiber finally');
}
});
$fiber->start();
throw new Exception('throw_in_fiber_finally');
}

function catches(callable $f) {
try {
$f();
} catch (Exception $e) {
echo 'caught: ', $e->getMessage(), $e->getPrevious() ? ', previous: '.$e->getPrevious()->getMessage() : '', "\n";
}
}

function catches_after_finally(callable $f) {
try {
try {
$f();
} finally {
echo "finally\n";
}
} catch (Exception $e) {
echo 'caught: ', $e->getMessage(), "\n";
}
}

foreach (['destructor_first', 'fiber_first', 'throw_in_fiber_finally'] as $f) {
echo "-- $f\n";
try {
catches($f);
catches_after_finally($f);
} catch (Throwable $e) {
echo 'escaped: ', $e->getMessage(), "\n";
}
}

?>
--EXPECT--
-- destructor_first
D::__destruct
caught: destructor_first
D::__destruct
finally
caught: destructor_first
-- fiber_first
D::__destruct
caught: fiber_first
D::__destruct
finally
caught: fiber_first
-- throw_in_fiber_finally
D::__destruct
caught: fiber finally, previous: throw_in_fiber_finally
D::__destruct
finally
caught: fiber finally
63 changes: 63 additions & 0 deletions Zend/tests/fibers/destructors_012.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
Fibers in destructors 012: GC destructor fiber running during exception unwinding does not skip the enclosing catch block
--INI--
zend.enable_gc=1
--FILE--
<?php

class D {
public function __destruct() {
echo "D::__destruct\n";
}
}

class Cycle {
public $self;
public function __construct() {
$this->self = $this;
}
public function __destruct() {
try {
throw new Exception('Cycle::__destruct');
} catch (Exception) {
echo "Cycle::__destruct\n";
}
}
}

$objects = [];
for ($i = 0; $i < 50000; $i++) {
$objects[] = new stdClass();
}

function f() {
global $objects;
$d = new D();
// Releasing these copies during unwinding fills the GC root buffer
$copies = [...$objects];
new Cycle();
throw new Exception('f');
}

function c() {
try {
f();
} catch (Exception $e) {
echo 'caught: ', $e->getMessage(), "\n";
}
}

$fiber = new Fiber(function () {
try {
c();
} catch (Throwable $e) {
echo 'escaped: ', $e->getMessage(), "\n";
}
});
$fiber->start();

?>
--EXPECT--
D::__destruct
Cycle::__destruct
caught: f
3 changes: 3 additions & 0 deletions Zend/zend_fibers.c
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ static void zend_fiber_object_destroy(zend_object *object)
}

zend_object *exception = EG(exception);
const zend_op *opline_before_exception = EG(opline_before_exception);
EG(exception) = NULL;

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

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

EG(opline_before_exception) = opline_before_exception;

zval_ptr_dtor(&graceful_exit);

if (transfer.flags & ZEND_FIBER_TRANSFER_FLAG_ERROR) {
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
GC_G(dtor_end) = GC_G(first_unused);

zend_object *exception = NULL;
const zend_op *opline_before_exception = EG(opline_before_exception);
remember_prev_exception(&exception);
zend_object *old_exception = exception;

Expand Down Expand Up @@ -1933,6 +1934,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
}

EG(exception) = exception;
EG(opline_before_exception) = opline_before_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
Expand Down
Loading