Prevent resumption of generator suspended in yield from

Normally we prevent generators from being resumed while they are already
running, but we failed to do so for generators delegating to non-Generators. As
a result such generator can be resumed, terminated, which causes unexpected
results (crashes) later.

In gh19306.phpt in particular, the generator delegate It::getIterator() suspends
while being called by generator g(). We then resume g(), which throws while
trying to resume It::getIterator(). This causes g() and It::getIterator()
to be released. We then UAF when resuming the Fiber in It::getIterator().

Fix this by ensuring that generators are marked as running while they fetch
the next value from the delegate.

Fixes GH-19306
Closes GH-19315
This commit is contained in:
Arnaud Le Blanc 2025-07-30 16:56:55 +02:00
parent 5bd5f352e5
commit 0406a55c92
No known key found for this signature in database
3 changed files with 45 additions and 2 deletions

2
NEWS
View file

@ -13,6 +13,8 @@ PHP NEWS
(Arnaud) (Arnaud)
. Fixed bug GH-19303 (Unpacking empty packed array into uninitialized array . Fixed bug GH-19303 (Unpacking empty packed array into uninitialized array
causes assertion failure). (nielsdos) causes assertion failure). (nielsdos)
. Fixed bug GH-19306 (Generator can be resumed while fetching next value from
delegated Generator). (Arnaud)
- FTP: - FTP:
. Fix theoretical issues with hrtime() not being available. (nielsdos) . Fix theoretical issues with hrtime() not being available. (nielsdos)

40
Zend/tests/gh19306.phpt Normal file
View file

@ -0,0 +1,40 @@
--TEST--
GH-19306: Generator suspended in yield from may be resumed
--FILE--
<?php
class It implements IteratorAggregate
{
public function getIterator(): Generator
{
yield "";
Fiber::suspend();
}
}
function g()
{
yield from new It();
}
$a = g();
$fiber = new Fiber(function () use ($a) {
echo "Fiber start\n";
$a->next();
echo "Fiber return\n";
});
$fiber->start();
echo "Fiber suspended\n";
try {
$a->next();
} catch (Throwable $t) {
echo $t->getMessage(), "\n";
}
echo "Destroying fiber\n";
$fiber = null;
echo "Shutdown\n";
?>
--EXPECT--
Fiber start
Fiber suspended
Cannot resume an already running generator
Destroying fiber
Shutdown

View file

@ -787,6 +787,8 @@ try_again:
orig_generator->execute_fake.prev_execute_data = original_execute_data; orig_generator->execute_fake.prev_execute_data = original_execute_data;
} }
generator->flags |= ZEND_GENERATOR_CURRENTLY_RUNNING;
/* Ensure this is run after executor_data swap to have a proper stack trace */ /* Ensure this is run after executor_data swap to have a proper stack trace */
if (UNEXPECTED(!Z_ISUNDEF(generator->values))) { if (UNEXPECTED(!Z_ISUNDEF(generator->values))) {
if (EXPECTED(zend_generator_get_next_delegated_value(generator) == SUCCESS)) { if (EXPECTED(zend_generator_get_next_delegated_value(generator) == SUCCESS)) {
@ -795,7 +797,7 @@ try_again:
EG(jit_trace_num) = original_jit_trace_num; EG(jit_trace_num) = original_jit_trace_num;
orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER); orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER);
generator->flags &= ~ZEND_GENERATOR_IN_FIBER; generator->flags &= ~(ZEND_GENERATOR_CURRENTLY_RUNNING | ZEND_GENERATOR_IN_FIBER);
return; return;
} }
if (UNEXPECTED(EG(exception))) { if (UNEXPECTED(EG(exception))) {
@ -817,7 +819,6 @@ try_again:
} }
/* Resume execution */ /* Resume execution */
generator->flags |= ZEND_GENERATOR_CURRENTLY_RUNNING;
if (!ZEND_OBSERVER_ENABLED) { if (!ZEND_OBSERVER_ENABLED) {
zend_execute_ex(generator->execute_data); zend_execute_ex(generator->execute_data);
} else { } else {