Commit 7b34e84bdb4 for php.net
commit 7b34e84bdb45e3856c5a47ae975ac901eeabf250
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Thu Aug 20 13:28:54 2026 +0500
Fix GH-23301: nested "yield from" repeats a value after "yield from []" (#23302)
The ZEND_GENERATOR_DO_INIT flag is added to a delegator to indicate that its next resume shouldn't advance an already initialized delegatee. The flag must be cleared on that next resume, but this was done on the wrong generator in some cases.
Fixes GH-23301.
Co-Authored-By: Arnaud Le Blanc <arnaud.lb@gmail.com>
diff --git a/NEWS b/NEWS
index e273a86dfca..a672360f0ae 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,8 @@ PHP NEWS
- Core:
. Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
next() call on the inner generator). (iliaal)
+ . Fixed bug GH-23301 (Nested "yield from" yields a value twice when the
+ middle generator delegates again). (Lazizbek Ergashev)
- DOM:
. Fixed a use-after-free when cloning a DOMNameSpaceNode after
diff --git a/Zend/tests/generators/backtrace_multi_yield_from.phpt b/Zend/tests/generators/backtrace_multi_yield_from.phpt
index 4fbaa2f05bb..2cb898b3aac 100644
--- a/Zend/tests/generators/backtrace_multi_yield_from.phpt
+++ b/Zend/tests/generators/backtrace_multi_yield_from.phpt
@@ -25,8 +25,8 @@ function from($gen) {
?>
--EXPECTF--
int(1)
-int(1)
#0 %s(10): gen()
#1 [internal function]: from(Object(Generator))
-#2 %s(19): Generator->next()
+#2 %s(17): Generator->next()
int(2)
+NULL
diff --git a/Zend/tests/generators/gh15375.phpt b/Zend/tests/generators/gh15375.phpt
index 48919083f22..f0a2fe56c16 100644
--- a/Zend/tests/generators/gh15375.phpt
+++ b/Zend/tests/generators/gh15375.phpt
@@ -46,9 +46,9 @@ function outer(Generator $inner) {
echo $s, "\n";
}
-// A shared, pre-primed generator consumed through two nested "yield from"
-// levels must still present its current value once to each consumer (the fix
-// must not over-clear the middle level's first-touch).
+// Reading a pre-primed generator through two nested "yield from" levels primes
+// the whole chain but advances nothing. A later next() on a middle level then
+// advances the shared generator, like any other next() would.
echo "shared primed:\n";
function counter() {
yield 1;
@@ -58,11 +58,11 @@ function counter() {
$gen1->valid();
$gen2 = outer($gen1);
$gen3 = outer($gen2);
-echo "gen3 current: ", $gen3->current(), "\n";
+var_dump($gen3->current());
$gen2->next();
-echo "gen2 current: ", $gen2->current(), "\n";
+var_dump($gen2->current());
$gen2->next();
-echo "gen2 current: ", $gen2->current(), "\n";
+var_dump($gen2->current());
?>
--EXPECT--
@@ -84,6 +84,6 @@ function counter() {
eight
nine
shared primed:
-gen3 current: 1
-gen2 current: 1
-gen2 current: 2
+int(1)
+int(2)
+NULL
diff --git a/Zend/tests/generators/gh23301.phpt b/Zend/tests/generators/gh23301.phpt
new file mode 100644
index 00000000000..02aaf2fe202
--- /dev/null
+++ b/Zend/tests/generators/gh23301.phpt
@@ -0,0 +1,44 @@
+--TEST--
+GH-23301 (Nested "yield from" yields a value twice when the middle generator delegates again)
+--FILE--
+<?php
+
+function inner() {
+ yield "B";
+}
+
+function middle() {
+ yield "A";
+ yield from inner();
+ yield "C";
+ yield from ["D"];
+}
+
+function delegate($gen) {
+ yield from $gen;
+}
+
+foreach (delegate(middle()) as $value) {
+ echo $value, "\n";
+}
+
+function tail($inner) {
+ yield from $inner;
+ yield from ["E"];
+}
+
+$middle = tail(inner());
+$outer = delegate($middle);
+
+var_dump($outer->current());
+$middle->next();
+var_dump($middle->current());
+
+?>
+--EXPECT--
+A
+B
+C
+D
+string(1) "B"
+string(1) "E"
diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c
index 83815773b91..ef645f8f9a3 100644
--- a/Zend/zend_generators.c
+++ b/Zend/zend_generators.c
@@ -776,10 +776,12 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
return;
}
- if (UNEXPECTED((delegator->flags & ZEND_GENERATOR_DO_INIT) != 0 && !Z_ISUNDEF(generator->value))) {
- /* We must not advance Generator if we yield from a Generator being currently run */
- orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
- return;
+ if (UNEXPECTED((delegator->flags & ZEND_GENERATOR_DO_INIT) != 0)) {
+ delegator->flags &= ~ZEND_GENERATOR_DO_INIT;
+ if (UNEXPECTED(!Z_ISUNDEF(generator->value))) {
+ /* We must not advance an already initialized delegate on first resumption */
+ return;
+ }
}
if (EG(active_fiber)) {
@@ -819,7 +821,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
EG(current_execute_data) = original_execute_data;
EG(jit_trace_num) = original_jit_trace_num;
- orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER);
+ orig_generator->flags &= ~ZEND_GENERATOR_IN_FIBER;
generator->flags &= ~(ZEND_GENERATOR_CURRENTLY_RUNNING | ZEND_GENERATOR_IN_FIBER);
return;
}
@@ -882,7 +884,6 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
} else {
generator = zend_generator_get_current(orig_generator);
zend_generator_throw_exception(generator, NULL);
- orig_generator->flags &= ~ZEND_GENERATOR_DO_INIT;
delegator = orig_generator;
goto try_again;
}
@@ -900,7 +901,7 @@ ZEND_API void zend_generator_resume(zend_generator *orig_generator) /* {{{ */
goto try_again;
}
- orig_generator->flags &= ~(ZEND_GENERATOR_DO_INIT | ZEND_GENERATOR_IN_FIBER);
+ orig_generator->flags &= ~ZEND_GENERATOR_IN_FIBER;
}
/* }}} */