Skip to content

Commit 67decbe

Browse files
authored
Fiber: hold the object a callable resolved to in the creating frame (#24134)
Inside a method of A, new Fiber([A::class, 'm']) or new Fiber("A::m") resolves the callable to the method's $this. Fiber::__construct() kept that object in fci.object and fci_cache.object without a reference, so once the A died before start(), the fiber called A::m() on freed memory. The fiber now holds fci.object for every callable, releases it with the callable (after the call, or when the fiber is freed unstarted) and reports it to the GC, so a fiber stored in a property of that object is a collectable cycle. A __call trampoline is not covered: it is resolved again at start(), against the frame that calls start().
1 parent 83b2eca commit 67decbe

3 files changed

Lines changed: 125 additions & 6 deletions

File tree

‎NEWS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ PHP NEWS
121121
the end of their mapping. (Ilia Alshanetsky)
122122
. Fixed bug GH-24050 (GC frees an object still held by a resurrected closure
123123
when (object) and use share an array). (David Carlier)
124+
. Fixed use-after-free when a Fiber callable resolves to $this of the method
125+
creating the Fiber ([A::class, 'm'] or "A::m") and that object dies before
126+
the Fiber runs. (Edmond Dantes)
124127
. Fixed exception thrown by destructor during GC in a Fiber not being rethrown
125128
into the frame that triggered the GC. (Nicolas Grekas)
126129
. Fixed bug GH-24139 (NULL pointer dereference in php_ini.c when
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
--TEST--
2+
Fiber holds the object a callable resolved to in the creating method ($this for [A::class, 'm'] or "A::m")
3+
--FILE--
4+
<?php
5+
class A {
6+
public ?Fiber $fiber = null;
7+
8+
public function __construct(public string $name) {}
9+
10+
public function m() {
11+
echo "m on {$this->name}\n";
12+
}
13+
14+
public function waits() {
15+
Fiber::suspend();
16+
echo "resumed on {$this->name}\n";
17+
}
18+
19+
public function make(string $method) {
20+
return new Fiber([A::class, $method]);
21+
}
22+
23+
public function makeFromString() {
24+
return new Fiber("A::m");
25+
}
26+
27+
public function keep() {
28+
$this->fiber = new Fiber([A::class, 'm']);
29+
}
30+
31+
public function __destruct() {
32+
echo "released {$this->name}\n";
33+
}
34+
}
35+
36+
echo "-- started after its creator's last reference went\n";
37+
$fiber = (new A("a1"))->make('m');
38+
$fiber->start();
39+
echo "after start\n";
40+
41+
echo "-- a string callable\n";
42+
$fiber = (new A("a2"))->makeFromString();
43+
$fiber->start();
44+
echo "after start\n";
45+
46+
echo "-- never started, released with the fiber\n";
47+
$fiber = (new A("a3"))->make('m');
48+
unset($fiber);
49+
echo "after unset\n";
50+
51+
echo "-- suspended while its object has no other reference\n";
52+
$a = new A("a4");
53+
$fiber = $a->make('waits');
54+
$fiber->start();
55+
unset($a);
56+
$fiber->resume();
57+
echo "after resume\n";
58+
59+
echo "-- a cycle through the fiber is collected\n";
60+
$a = new A("a5");
61+
$a->keep();
62+
unset($a);
63+
var_dump(gc_collect_cycles());
64+
echo "end\n";
65+
?>
66+
--EXPECT--
67+
-- started after its creator's last reference went
68+
m on a1
69+
released a1
70+
after start
71+
-- a string callable
72+
m on a2
73+
released a2
74+
after start
75+
-- never started, released with the fiber
76+
released a3
77+
after unset
78+
-- suspended while its object has no other reference
79+
resumed on a4
80+
released a4
81+
after resume
82+
-- a cycle through the fiber is collected
83+
released a5
84+
int(2)
85+
end

‎Zend/zend_fibers.c‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,39 @@ static void zend_fiber_cleanup(zend_fiber_context *context)
564564
fiber->caller = NULL;
565565
}
566566

567+
static void zend_fiber_addref_callable(zend_fiber *fiber)
568+
{
569+
// Keep a reference to closures or callable objects while the fiber is running.
570+
Z_TRY_ADDREF(fiber->fci.function_name);
571+
572+
/* For [A::class, 'm'] or "A::m" inside a method of A, fci.object is the caller's $this. */
573+
if (fiber->fci.object != NULL) {
574+
GC_ADDREF(fiber->fci.object);
575+
}
576+
}
577+
578+
static void zend_fiber_get_gc_callable(zend_fiber *fiber, zend_get_gc_buffer *buf)
579+
{
580+
zend_get_gc_buffer_add_zval(buf, &fiber->fci.function_name);
581+
582+
if (fiber->fci.object != NULL) {
583+
zend_get_gc_buffer_add_obj(buf, fiber->fci.object);
584+
}
585+
}
586+
587+
static void zend_fiber_release_callable(zend_fiber *fiber)
588+
{
589+
zval_ptr_dtor(&fiber->fci.function_name);
590+
ZVAL_UNDEF(&fiber->fci.function_name);
591+
fiber->fci_cache = empty_fcall_info_cache;
592+
593+
if (fiber->fci.object != NULL) {
594+
zend_object *object = fiber->fci.object;
595+
fiber->fci.object = NULL;
596+
OBJ_RELEASE(object);
597+
}
598+
}
599+
567600
static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer)
568601
{
569602
ZEND_ASSERT(Z_TYPE(transfer->value) == IS_NULL && "Initial transfer value to fiber context must be NULL");
@@ -609,8 +642,7 @@ static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer)
609642
zend_call_function(&fiber->fci, &fiber->fci_cache);
610643

611644
/* Cleanup callback and unset field to prevent GC / duplicate dtor issues. */
612-
zval_ptr_dtor(&fiber->fci.function_name);
613-
ZVAL_UNDEF(&fiber->fci.function_name);
645+
zend_fiber_release_callable(fiber);
614646

615647
if (EG(exception)) {
616648
if (!(fiber->flags & ZEND_FIBER_FLAG_DESTROYED)
@@ -812,7 +844,7 @@ static void zend_fiber_object_free(zend_object *object)
812844
{
813845
zend_fiber *fiber = (zend_fiber *) object;
814846

815-
zval_ptr_dtor(&fiber->fci.function_name);
847+
zend_fiber_release_callable(fiber);
816848
zval_ptr_dtor(&fiber->result);
817849

818850
zend_object_std_dtor(&fiber->std);
@@ -823,7 +855,7 @@ static HashTable *zend_fiber_object_gc(zend_object *object, zval **table, int *n
823855
zend_fiber *fiber = (zend_fiber *) object;
824856
zend_get_gc_buffer *buf = zend_get_gc_buffer_create();
825857

826-
zend_get_gc_buffer_add_zval(buf, &fiber->fci.function_name);
858+
zend_fiber_get_gc_callable(fiber, buf);
827859
zend_get_gc_buffer_add_zval(buf, &fiber->result);
828860

829861
if (fiber->context.status != ZEND_FIBER_STATUS_SUSPENDED || fiber->caller != NULL) {
@@ -891,8 +923,7 @@ ZEND_METHOD(Fiber, __construct)
891923
fiber->fci = fci;
892924
fiber->fci_cache = fcc;
893925

894-
// Keep a reference to closures or callable objects while the fiber is running.
895-
Z_TRY_ADDREF(fiber->fci.function_name);
926+
zend_fiber_addref_callable(fiber);
896927
}
897928

898929
ZEND_METHOD(Fiber, start)

0 commit comments

Comments
 (0)