Skip to content

Do not scan generator frames more than once #15330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 28, 2024
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
38 changes: 38 additions & 0 deletions Zend/tests/gh15330-001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
GH-15330 001: Do not scan generator frames more than once
--FILE--
<?php

class It implements \IteratorAggregate
{
public function getIterator(): \Generator
{
yield 'foo';
Fiber::suspend();
var_dump("not executed");
}
}

function f() {
var_dump(yield from new It());
}

$iterable = f();

$fiber = new Fiber(function () use ($iterable) {
var_dump($iterable->current());
$iterable->next();
var_dump("not executed");
});

$ref = $fiber;

$fiber->start();

gc_collect_cycles();

?>
==DONE==
--EXPECT--
string(3) "foo"
==DONE==
33 changes: 33 additions & 0 deletions Zend/tests/gh15330-002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
GH-15330 002: Do not scan generator frames more than once
--FILE--
<?php

function g() {
yield 'foo';
Fiber::suspend();
}

function f() {
var_dump(yield from g());
}

$iterable = f();

$fiber = new Fiber(function () use ($iterable) {
var_dump($iterable->current());
$iterable->next();
var_dump("not executed");
});

$ref = $fiber;

$fiber->start();

gc_collect_cycles();

?>
==DONE==
--EXPECT--
string(3) "foo"
==DONE==
57 changes: 57 additions & 0 deletions Zend/tests/gh15330-003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
GH-15330 003: Do not scan generator frames more than once
--FILE--
<?php

class It implements \IteratorAggregate
{
public function getIterator(): \Generator
{
yield 'foo';
Fiber::suspend();
var_dump("not executed");
}
}

class Canary {
public function __construct(public mixed $value) {}
public function __destruct() {
var_dump(__METHOD__);
}
}

function f($canary) {
var_dump(yield from new It());
}

$canary = new Canary(null);

$iterable = f($canary);

$fiber = new Fiber(function () use ($iterable, $canary) {
var_dump($canary, $iterable->current());
$iterable->next();
var_dump("not executed");
});

$canary->value = $fiber;

$fiber->start();

$iterable->current();

$fiber = $iterable = $canary = null;

gc_collect_cycles();

?>
==DONE==
--EXPECTF--
object(Canary)#%d (1) {
["value"]=>
object(Fiber)#%d (0) {
}
}
string(3) "foo"
string(18) "Canary::__destruct"
==DONE==
52 changes: 52 additions & 0 deletions Zend/tests/gh15330-004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
GH-15330 004: Do not scan generator frames more than once
--FILE--
<?php

class Canary {
public function __construct(public mixed $value) {}
public function __destruct() {
var_dump(__METHOD__);
}
}

function g() {
yield 'foo';
Fiber::suspend();
}

function f($canary) {
var_dump(yield from g());
}

$canary = new Canary(null);

$iterable = f($canary);

$fiber = new Fiber(function () use ($iterable, $canary) {
var_dump($canary, $iterable->current());
$iterable->next();
var_dump("not executed");
});

$canary->value = $fiber;

$fiber->start();

$iterable->current();

$fiber = $iterable = $canary = null;

gc_collect_cycles();

?>
==DONE==
--EXPECTF--
object(Canary)#%d (1) {
["value"]=>
object(Fiber)#%d (0) {
}
}
string(3) "foo"
string(18) "Canary::__destruct"
==DONE==
53 changes: 53 additions & 0 deletions Zend/tests/gh15330-005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
GH-15330 005: Do not scan generator frames more than once
--FILE--
<?php

class Canary {
public function __construct(public mixed $value) {}
public function __destruct() {
var_dump(__METHOD__);
}
}

function g() {
yield 'foo';
Fiber::suspend();
}

function f($canary) {
var_dump(yield from g());
}

$canary = new Canary(null);

$iterable = f($canary);

$fiber = new Fiber(function () use ($iterable, $canary) {
var_dump($canary, $iterable->current());
$f = $iterable->next(...);
$f();
var_dump("not executed");
});

$canary->value = $fiber;

$fiber->start();

$iterable->current();

$fiber = $iterable = $canary = null;

gc_collect_cycles();

?>
==DONE==
--EXPECTF--
object(Canary)#%d (1) {
["value"]=>
object(Fiber)#%d (0) {
}
}
string(3) "foo"
string(18) "Canary::__destruct"
==DONE==
56 changes: 56 additions & 0 deletions Zend/tests/gh15330-006.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
GH-15330 006: Do not scan generator frames more than once
--FILE--
<?php

class Canary {
public function __construct(public mixed $value) {}
public function __destruct() {
var_dump(__METHOD__);
}
}

function h() {
yield 'foo';
Fiber::suspend();
}

function g() {
yield from h();
}

function f($canary) {
var_dump(yield from g());
}

$canary = new Canary(null);

$iterable = f($canary);

$fiber = new Fiber(function () use ($iterable, $canary) {
var_dump($canary, $iterable->current());
$iterable->next();
var_dump("not executed");
});

$canary->value = $fiber;

$fiber->start();

$iterable->current();

$fiber = $iterable = $canary = null;

gc_collect_cycles();

?>
==DONE==
--EXPECTF--
object(Canary)#%d (1) {
["value"]=>
object(Fiber)#%d (0) {
}
}
string(3) "foo"
string(18) "Canary::__destruct"
==DONE==
21 changes: 14 additions & 7 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -4437,7 +4437,20 @@ ZEND_API ZEND_ATTRIBUTE_DEPRECATED HashTable *zend_unfinished_execution_gc(zend_

ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer, bool suspended_by_yield)
{
if (!EX(func) || !ZEND_USER_CODE(EX(func)->common.type)) {
if (!EX(func)) {
return NULL;
}

if (EX_CALL_INFO() & ZEND_CALL_RELEASE_THIS) {
zend_get_gc_buffer_add_obj(gc_buffer, Z_OBJ(execute_data->This));
}

if (EX_CALL_INFO() & ZEND_CALL_CLOSURE) {
zend_get_gc_buffer_add_obj(gc_buffer, ZEND_CLOSURE_OBJECT(EX(func)));
}

if (!ZEND_USER_CODE(EX(func)->common.type)) {
ZEND_ASSERT(!(EX_CALL_INFO() & (ZEND_CALL_HAS_SYMBOL_TABLE|ZEND_CALL_FREE_EXTRA_ARGS|ZEND_CALL_HAS_EXTRA_NAMED_PARAMS)));
return NULL;
}

Expand All @@ -4458,12 +4471,6 @@ ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_d
}
}

if (EX_CALL_INFO() & ZEND_CALL_RELEASE_THIS) {
zend_get_gc_buffer_add_obj(gc_buffer, Z_OBJ(execute_data->This));
}
if (EX_CALL_INFO() & ZEND_CALL_CLOSURE) {
zend_get_gc_buffer_add_obj(gc_buffer, ZEND_CLOSURE_OBJECT(EX(func)));
}
if (EX_CALL_INFO() & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) {
zval extra_named_params;
ZVAL_ARR(&extra_named_params, EX(extra_named_params));
Expand Down
22 changes: 21 additions & 1 deletion Zend/zend_fibers.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "zend.h"
#include "zend_API.h"
#include "zend_gc.h"
#include "zend_ini.h"
#include "zend_vm.h"
#include "zend_exceptions.h"
Expand All @@ -27,6 +28,7 @@
#include "zend_mmap.h"
#include "zend_compile.h"
#include "zend_closures.h"
#include "zend_generators.h"

#include "zend_fibers.h"
#include "zend_fibers_arginfo.h"
Expand Down Expand Up @@ -682,7 +684,25 @@ static HashTable *zend_fiber_object_gc(zend_object *object, zval **table, int *n
HashTable *lastSymTable = NULL;
zend_execute_data *ex = fiber->execute_data;
for (; ex; ex = ex->prev_execute_data) {
HashTable *symTable = zend_unfinished_execution_gc_ex(ex, ex->func && ZEND_USER_CODE(ex->func->type) ? ex->call : NULL, buf, false);
HashTable *symTable;
if (ZEND_CALL_INFO(ex) & ZEND_CALL_GENERATOR) {
/* The generator object is stored in ex->return_value */
zend_generator *generator = (zend_generator*)ex->return_value;
/* There are two cases to consider:
* - If the generator is currently running, the Generator's GC
* handler will ignore it because it is not collectable. However,
* in this context the generator is suspended in Fiber::suspend()
* and may be collectable, so we can inspect it.
* - If the generator is not running, the Generator's GC handler
* will inspect it. In this case we have to skip the frame.
*/
if (!(generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can a generator be not running and on the fiber stacktrace at the same time? As far as I'm know, running generator <=> generator exists on exactly one stacktrace?
Isn't that check redundant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generators stopped in yield from do not have the ZEND_GENERATOR_CURRENTLY_RUNNING flag. We consider them to be running in most places because we check zend_generator_get_current(generator)->flags instead of generator->flags.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, I forgot about the placeholder frame.

continue;
}
symTable = zend_generator_frame_gc(buf, generator);
} else {
symTable = zend_unfinished_execution_gc_ex(ex, ex->func && ZEND_USER_CODE(ex->func->type) ? ex->call : NULL, buf, false);
}
if (symTable) {
if (lastSymTable) {
zval *val;
Expand Down
Loading
Loading