From a26f3c7896f57e07b498c34f9e5ec6c0b27628a0 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Mon, 3 Aug 2026 06:30:04 +0800 Subject: [PATCH 1/8] Fix GH-11591: Handle empty generators in AppendIterator --- NEWS | 2 + ext/spl/spl_iterators.c | 76 +++++++++++++++-- ext/spl/tests/gh11591.phpt | 51 +++++++++++ ext/spl/tests/gh11591_2.phpt | 159 +++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 ext/spl/tests/gh11591.phpt create mode 100644 ext/spl/tests/gh11591_2.phpt diff --git a/NEWS b/NEWS index 4e95bed34c84..704abf662454 100644 --- a/NEWS +++ b/NEWS @@ -520,6 +520,8 @@ PHP NEWS SodiumException. (iliaal) - SPL: + . Fixed bug GH-11591 (AppendIterator fails with an empty generator). + (Matthias Görgens) . DirectoryIterator key can now work better with filesystem supporting larger directory indexing. (David Carlier) . Fixed bug GH-21831 (SplObjectStorage::removeAllExcept() use-after-free with diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c index 06d344990dfa..55922a4b7d7a 100644 --- a/ext/spl/spl_iterators.c +++ b/ext/spl/spl_iterators.c @@ -18,7 +18,9 @@ #include "php.h" #include "zend_exceptions.h" +#include "zend_generators.h" #include "zend_interfaces.h" +#include "zend_weakrefs.h" #include "ext/pcre/php_pcre.h" #include "spl_iterators.h" @@ -119,7 +121,9 @@ typedef struct _spl_dual_it_object { } caching; struct { zval zarrayit; + zval current_key; zend_object_iterator *iterator; + HashTable *empty_generators; } append; struct { zend_long flags; @@ -2024,6 +2028,13 @@ static void spl_dual_it_free_storage(zend_object *_object) if (object->dit_type == DIT_AppendIterator) { zend_iterator_dtor(object->u.append.iterator); + if (object->u.append.empty_generators) { + zend_weakrefs_hash_destroy(object->u.append.empty_generators); + FREE_HASHTABLE(object->u.append.empty_generators); + } + if (!Z_ISUNDEF(object->u.append.current_key)) { + zval_ptr_dtor(&object->u.append.current_key); + } if (Z_TYPE(object->u.append.zarrayit) != IS_UNDEF) { zval_ptr_dtor(&object->u.append.zarrayit); } @@ -2767,6 +2778,10 @@ PHP_METHOD(EmptyIterator, next) static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{{*/ { spl_dual_it_free(intern); + if (!Z_ISUNDEF(intern->u.append.current_key)) { + zval_ptr_dtor(&intern->u.append.current_key); + ZVAL_UNDEF(&intern->u.append.current_key); + } if (!Z_ISUNDEF(intern->inner.zobject)) { zval_ptr_dtor(&intern->inner.zobject); @@ -2783,6 +2798,20 @@ static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{ it = intern->u.append.iterator->funcs->get_current_data(intern->u.append.iterator); ZVAL_COPY(&intern->inner.zobject, it); intern->inner.ce = Z_OBJCE_P(it); + intern->u.append.iterator->funcs->get_current_key( + intern->u.append.iterator, &intern->u.append.current_key); + if (intern->u.append.empty_generators) { + zval *keys = zend_hash_index_find(intern->u.append.empty_generators, + zend_object_to_weakref_key(Z_OBJ_P(it))); + if (keys) { + bool known_empty = Z_TYPE(intern->u.append.current_key) == IS_LONG + ? zend_hash_index_exists(Z_ARRVAL_P(keys), Z_LVAL(intern->u.append.current_key)) + : zend_hash_exists(Z_ARRVAL_P(keys), Z_STR(intern->u.append.current_key)); + if (known_empty) { + return SUCCESS; + } + } + } intern->inner.iterator = intern->inner.ce->get_iterator(intern->inner.ce, it, 0); spl_dual_it_rewind(intern); return SUCCESS; @@ -2791,9 +2820,44 @@ static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{ } } /* }}} */ -static void spl_append_it_fetch(spl_dual_it_object *intern) /* {{{*/ +static void spl_append_it_record_empty_generator(spl_dual_it_object *intern) /* {{{ */ { - while (spl_dual_it_valid(intern) != SUCCESS) { + if (EG(exception)) { + return; + } + + if (!intern->u.append.empty_generators) { + ALLOC_HASHTABLE(intern->u.append.empty_generators); + zend_hash_init(intern->u.append.empty_generators, 0, NULL, ZVAL_PTR_DTOR, 0); + } + + zval *keys = zend_hash_index_find(intern->u.append.empty_generators, + zend_object_to_weakref_key(Z_OBJ(intern->inner.zobject))); + if (!keys) { + zval new_keys; + array_init(&new_keys); + keys = zend_weakrefs_hash_add(intern->u.append.empty_generators, + Z_OBJ(intern->inner.zobject), &new_keys); + ZEND_ASSERT(keys != NULL); + } + + if (Z_TYPE(intern->u.append.current_key) == IS_LONG) { + zend_hash_index_add_empty_element(Z_ARRVAL_P(keys), Z_LVAL(intern->u.append.current_key)); + } else { + zend_hash_add_empty_element(Z_ARRVAL_P(keys), Z_STR(intern->u.append.current_key)); + } +} /* }}} */ + +static void spl_append_it_fetch(spl_dual_it_object *intern, bool record_empty) /* {{{*/ +{ + while (true) { + bool may_record = record_empty && intern->inner.ce == zend_ce_generator; + if (spl_dual_it_valid(intern) == SUCCESS) { + break; + } + if (may_record) { + spl_append_it_record_empty_generator(intern); + } intern->u.append.iterator->funcs->move_forward(intern->u.append.iterator); if (spl_append_it_next_iterator(intern) != SUCCESS) { return; @@ -2807,7 +2871,7 @@ static void spl_append_it_next(spl_dual_it_object *intern) /* {{{ */ if (spl_dual_it_valid(intern) == SUCCESS) { spl_dual_it_next(intern, 1); } - spl_append_it_fetch(intern); + spl_append_it_fetch(intern, false); } /* }}} */ /* {{{ Create an AppendIterator */ @@ -2824,6 +2888,8 @@ PHP_METHOD(AppendIterator, __construct) } intern->dit_type = DIT_AppendIterator; + ZVAL_UNDEF(&intern->u.append.current_key); + intern->u.append.empty_generators = NULL; object_init_with_constructor(&intern->u.append.zarrayit, spl_ce_ArrayIterator, 0, NULL, NULL); intern->u.append.iterator = spl_ce_ArrayIterator->get_iterator(spl_ce_ArrayIterator, &intern->u.append.zarrayit, 0); @@ -2855,7 +2921,7 @@ PHP_METHOD(AppendIterator, append) do { spl_append_it_next_iterator(intern); } while (Z_OBJ(intern->inner.zobject) != Z_OBJ_P(it)); - spl_append_it_fetch(intern); + spl_append_it_fetch(intern, true); } } /* }}} */ @@ -2887,7 +2953,7 @@ PHP_METHOD(AppendIterator, rewind) intern->u.append.iterator->funcs->rewind(intern->u.append.iterator); if (spl_append_it_next_iterator(intern) == SUCCESS) { - spl_append_it_fetch(intern); + spl_append_it_fetch(intern, false); } } /* }}} */ diff --git a/ext/spl/tests/gh11591.phpt b/ext/spl/tests/gh11591.phpt new file mode 100644 index 000000000000..7a81f5bb594c --- /dev/null +++ b/ext/spl/tests/gh11591.phpt @@ -0,0 +1,51 @@ +--TEST-- +GH-11591 (AppendIterator with generators that yield nothing) +--FILE-- +append(emptyGenerator()); +values($iterator); +values($iterator); + +echo "leading\n"; +$iterator = new AppendIterator(); +$iterator->append(emptyGenerator()); +$iterator->append(new ArrayIterator(['A'])); +values($iterator); +values($iterator); + +echo "distinct\n"; +$iterator = new AppendIterator(); +$iterator->append(emptyGenerator()); +$iterator->append(emptyGenerator()); +values($iterator); +?> +--EXPECT-- +sole +array(0) { +} +array(0) { +} +leading +array(1) { + [0]=> + string(1) "A" +} +array(1) { + [0]=> + string(1) "A" +} +distinct +array(0) { +} diff --git a/ext/spl/tests/gh11591_2.phpt b/ext/spl/tests/gh11591_2.phpt new file mode 100644 index 000000000000..244460189539 --- /dev/null +++ b/ext/spl/tests/gh11591_2.phpt @@ -0,0 +1,159 @@ +--TEST-- +GH-11591 (AppendIterator preserves other iterator semantics) +--FILE-- +getMessage(), "\n"; + } +} + +echo "duplicate\n"; +$generator = emptyGenerator(); +$iterator = new AppendIterator(); +$iterator->append($generator); +try { + $iterator->append($generator); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +echo "replacement\n"; +$iterator = new AppendIterator(); +$iterator->append(emptyGenerator()); +$iterator->getArrayIterator()[0] = new ArrayIterator(['R']); +values($iterator); + +echo "ordinary\n"; +$ordinary = new class implements Iterator { + public int $rewinds = 0; + public function rewind(): void { $this->rewinds++; } + public function valid(): bool { return false; } + public function current(): mixed { return null; } + public function key(): mixed { return null; } + public function next(): void {} +}; +$iterator = new AppendIterator(); +$iterator->append($ordinary); +echo $ordinary->rewinds, "\n"; +values($iterator); +echo $ordinary->rewinds, "\n"; +values($iterator); +echo $ordinary->rewinds, "\n"; + +echo "nonempty\n"; +$generator = (function () { yield 'G'; })(); +$iterator = new AppendIterator(); +$iterator->append($generator); +values($iterator); +values($iterator); + +echo "moved\n"; +$generator = emptyGenerator(); +$iterator = new AppendIterator(); +$iterator->append($generator); +$array = $iterator->getArrayIterator(); +unset($array[0]); +$array[] = $generator; +values($iterator); + +echo "append during traversal\n"; +$iterator = new AppendIterator(); +$iterator->append(new ArrayIterator(['A'])); +$added = false; +foreach ($iterator as $value) { + echo $value; + if (!$added) { + $iterator->append(emptyGenerator()); + $iterator->append(new ArrayIterator(['B'])); + $added = true; + } +} +echo "\n"; + +echo "externally closed\n"; +$generator = emptyGenerator(); +$generator->valid(); +$iterator = new AppendIterator(); +try { + $iterator->append($generator); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +echo "weak lifetime\n"; +$generator = emptyGenerator(); +$weak = WeakReference::create($generator); +$iterator = new AppendIterator(); +$iterator->append($generator); +unset($iterator->getArrayIterator()[0], $generator); +gc_collect_cycles(); +var_dump($weak->get() === null); + +echo "reentrant unset\n"; +$iterator = new AppendIterator(); +$generator = (function () use (&$iterator): Generator { + unset($iterator->getArrayIterator()[0]); + if (false) { + yield; + } +})(); +$iterator->append($generator); +values($iterator); + +echo "reentrant move\n"; +$iterator = new AppendIterator(); +$generator = (function () use (&$iterator, &$generator): Generator { + $array = $iterator->getArrayIterator(); + unset($array[0]); + $array[1] = $generator; + if (false) { + yield; + } +})(); +$iterator->append($generator); +values($iterator); +?> +--EXPECT-- +duplicate +Cannot traverse an already closed generator +replacement +array(1) { + [0]=> + string(1) "R" +} +ordinary +1 +array(0) { +} +2 +array(0) { +} +3 +nonempty +array(1) { + [0]=> + string(1) "G" +} +Cannot traverse an already closed generator +moved +Cannot traverse an already closed generator +append during traversal +AB +externally closed +Cannot traverse an already closed generator +weak lifetime +bool(true) +reentrant unset +array(0) { +} +reentrant move +Cannot traverse an already closed generator From f531e99dd1854fa538804ee9d91190ba8fe1005a Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 09:54:00 +0800 Subject: [PATCH 2/8] Mark probed-empty generators by object, not entry key Reinserting the same generator at the same outer key after an unset inherited a stale empty marker keyed by (object, key), silently skipping the entry where reinsertion at any other key threw. A generator can only enter the iterator through append()'s own probe, so marking the object itself is equivalent and cannot go stale. --- ext/spl/spl_iterators.c | 31 +++++++------------- ext/spl/tests/gh11591_3.phpt | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 ext/spl/tests/gh11591_3.phpt diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c index 55922a4b7d7a..0447adf65757 100644 --- a/ext/spl/spl_iterators.c +++ b/ext/spl/spl_iterators.c @@ -2801,15 +2801,10 @@ static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{ intern->u.append.iterator->funcs->get_current_key( intern->u.append.iterator, &intern->u.append.current_key); if (intern->u.append.empty_generators) { - zval *keys = zend_hash_index_find(intern->u.append.empty_generators, + zval *dummy = zend_hash_index_find(intern->u.append.empty_generators, zend_object_to_weakref_key(Z_OBJ_P(it))); - if (keys) { - bool known_empty = Z_TYPE(intern->u.append.current_key) == IS_LONG - ? zend_hash_index_exists(Z_ARRVAL_P(keys), Z_LVAL(intern->u.append.current_key)) - : zend_hash_exists(Z_ARRVAL_P(keys), Z_STR(intern->u.append.current_key)); - if (known_empty) { - return SUCCESS; - } + if (dummy) { + return SUCCESS; } } intern->inner.iterator = intern->inner.ce->get_iterator(intern->inner.ce, it, 0); @@ -2831,20 +2826,14 @@ static void spl_append_it_record_empty_generator(spl_dual_it_object *intern) /* zend_hash_init(intern->u.append.empty_generators, 0, NULL, ZVAL_PTR_DTOR, 0); } - zval *keys = zend_hash_index_find(intern->u.append.empty_generators, + zval *dummy = zend_hash_index_find(intern->u.append.empty_generators, zend_object_to_weakref_key(Z_OBJ(intern->inner.zobject))); - if (!keys) { - zval new_keys; - array_init(&new_keys); - keys = zend_weakrefs_hash_add(intern->u.append.empty_generators, - Z_OBJ(intern->inner.zobject), &new_keys); - ZEND_ASSERT(keys != NULL); - } - - if (Z_TYPE(intern->u.append.current_key) == IS_LONG) { - zend_hash_index_add_empty_element(Z_ARRVAL_P(keys), Z_LVAL(intern->u.append.current_key)); - } else { - zend_hash_add_empty_element(Z_ARRVAL_P(keys), Z_STR(intern->u.append.current_key)); + if (!dummy) { + zval empty_zval; + ZVAL_NULL(&empty_zval); + dummy = zend_weakrefs_hash_add(intern->u.append.empty_generators, + Z_OBJ(intern->inner.zobject), &empty_zval); + ZEND_ASSERT(dummy != NULL); } } /* }}} */ diff --git a/ext/spl/tests/gh11591_3.phpt b/ext/spl/tests/gh11591_3.phpt new file mode 100644 index 000000000000..241638794291 --- /dev/null +++ b/ext/spl/tests/gh11591_3.phpt @@ -0,0 +1,55 @@ +--TEST-- +GH-11591 (AppendIterator skips probed-empty generators after array mutations) +--FILE-- +getMessage(), "\n"; + } +} + +echo "reinsert same key\n"; +$generator = emptyGenerator(); +$iterator = new AppendIterator(); +$iterator->append($generator); +$outer = $iterator->getArrayIterator(); +$outer->offsetUnset(0); +$outer->offsetSet(0, $generator); +values($iterator); + +echo "reinsert other key\n"; +$generator = emptyGenerator(); +$iterator = new AppendIterator(); +$iterator->append($generator); +$outer = $iterator->getArrayIterator(); +$outer->offsetUnset(0); +$outer->offsetSet(1, $generator); +values($iterator); + +echo "replace with unstarted generator\n"; +$generator = emptyGenerator(); +$iterator = new AppendIterator(); +$iterator->append($generator); +$outer = $iterator->getArrayIterator(); +$outer->offsetUnset(0); +$outer->offsetSet(0, emptyGenerator()); +values($iterator); +?> +--EXPECT-- +reinsert same key +array(0) { +} +reinsert other key +array(0) { +} +replace with unstarted generator +array(0) { +} From 3e307f8d828db22993bcb987e4097f933bd0bd7f Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 09:55:58 +0800 Subject: [PATCH 3/8] Expect probed-empty generators to be skipped wherever they sit Moving a probed-empty generator to another key previously threw, because the marker followed the entry key rather than the generator object. --- ext/spl/tests/gh11591_2.phpt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/spl/tests/gh11591_2.phpt b/ext/spl/tests/gh11591_2.phpt index 244460189539..6b2d529772c8 100644 --- a/ext/spl/tests/gh11591_2.phpt +++ b/ext/spl/tests/gh11591_2.phpt @@ -145,7 +145,8 @@ array(1) { } Cannot traverse an already closed generator moved -Cannot traverse an already closed generator +array(0) { +} append during traversal AB externally closed @@ -156,4 +157,5 @@ reentrant unset array(0) { } reentrant move -Cannot traverse an already closed generator +array(0) { +} From af4315ddd4cd13568fa486dee2f3d2d55caf6b68 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 09:57:05 +0800 Subject: [PATCH 4/8] Appending a probed-empty generator twice is now two empty entries --- ext/spl/tests/gh11591_2.phpt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ext/spl/tests/gh11591_2.phpt b/ext/spl/tests/gh11591_2.phpt index 6b2d529772c8..9c8c5b245cca 100644 --- a/ext/spl/tests/gh11591_2.phpt +++ b/ext/spl/tests/gh11591_2.phpt @@ -20,11 +20,8 @@ echo "duplicate\n"; $generator = emptyGenerator(); $iterator = new AppendIterator(); $iterator->append($generator); -try { - $iterator->append($generator); -} catch (Throwable $e) { - echo $e->getMessage(), "\n"; -} +$iterator->append($generator); +values($iterator); echo "replacement\n"; $iterator = new AppendIterator(); @@ -124,7 +121,8 @@ values($iterator); ?> --EXPECT-- duplicate -Cannot traverse an already closed generator +array(0) { +} replacement array(1) { [0]=> From 589bddc7f019f3660b688c18646ef4d9601f098b Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 10:14:47 +0800 Subject: [PATCH 5/8] Skip empty generators wherever they are first exhausted An empty generator appended after a valid entry was never probed by append(), so its first exhaustion during traversal left it unmarked and a second traversal still threw. Mark generators that finish while initialising (they never yielded) at every exhaustion point, keyed by object so reinsertion cannot inherit a stale marker. --- ext/spl/spl_iterators.c | 19 ++++++++++++------- ext/spl/tests/gh11591_3.phpt | 26 ++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c index 0447adf65757..00cec508ac1b 100644 --- a/ext/spl/spl_iterators.c +++ b/ext/spl/spl_iterators.c @@ -2837,15 +2837,20 @@ static void spl_append_it_record_empty_generator(spl_dual_it_object *intern) /* } } /* }}} */ -static void spl_append_it_fetch(spl_dual_it_object *intern, bool record_empty) /* {{{*/ +static void spl_append_it_fetch(spl_dual_it_object *intern) /* {{{*/ { while (true) { - bool may_record = record_empty && intern->inner.ce == zend_ce_generator; if (spl_dual_it_valid(intern) == SUCCESS) { break; } - if (may_record) { - spl_append_it_record_empty_generator(intern); + if (intern->inner.ce == zend_ce_generator) { + zend_generator *generator = (zend_generator *) Z_OBJ(intern->inner.zobject); + /* A generator that finished while initialising never yielded, so it is + * empty and can be skipped. One that yielded was consumed and still + * reports "Cannot rewind a generator that was already run". */ + if (generator->flags & ZEND_GENERATOR_AT_FIRST_YIELD) { + spl_append_it_record_empty_generator(intern); + } } intern->u.append.iterator->funcs->move_forward(intern->u.append.iterator); if (spl_append_it_next_iterator(intern) != SUCCESS) { @@ -2860,7 +2865,7 @@ static void spl_append_it_next(spl_dual_it_object *intern) /* {{{ */ if (spl_dual_it_valid(intern) == SUCCESS) { spl_dual_it_next(intern, 1); } - spl_append_it_fetch(intern, false); + spl_append_it_fetch(intern); } /* }}} */ /* {{{ Create an AppendIterator */ @@ -2910,7 +2915,7 @@ PHP_METHOD(AppendIterator, append) do { spl_append_it_next_iterator(intern); } while (Z_OBJ(intern->inner.zobject) != Z_OBJ_P(it)); - spl_append_it_fetch(intern, true); + spl_append_it_fetch(intern); } } /* }}} */ @@ -2942,7 +2947,7 @@ PHP_METHOD(AppendIterator, rewind) intern->u.append.iterator->funcs->rewind(intern->u.append.iterator); if (spl_append_it_next_iterator(intern) == SUCCESS) { - spl_append_it_fetch(intern, false); + spl_append_it_fetch(intern); } } /* }}} */ diff --git a/ext/spl/tests/gh11591_3.phpt b/ext/spl/tests/gh11591_3.phpt index 241638794291..660c88e2bd24 100644 --- a/ext/spl/tests/gh11591_3.phpt +++ b/ext/spl/tests/gh11591_3.phpt @@ -34,13 +34,20 @@ $outer->offsetUnset(0); $outer->offsetSet(1, $generator); values($iterator); -echo "replace with unstarted generator\n"; +echo "replace with non-empty generator\n"; $generator = emptyGenerator(); $iterator = new AppendIterator(); $iterator->append($generator); $outer = $iterator->getArrayIterator(); $outer->offsetUnset(0); -$outer->offsetSet(0, emptyGenerator()); +$outer->offsetSet(0, (function () { yield 'NEW'; })()); +values($iterator); + +echo "empty appended after valid entry\n"; +$iterator = new AppendIterator(); +$iterator->append(new ArrayIterator(['A'])); +$iterator->append(emptyGenerator()); +values($iterator); values($iterator); ?> --EXPECT-- @@ -50,6 +57,17 @@ array(0) { reinsert other key array(0) { } -replace with unstarted generator -array(0) { +replace with non-empty generator +array(1) { + [0]=> + string(3) "NEW" +} +empty appended after valid entry +array(1) { + [0]=> + string(1) "A" +} +array(1) { + [0]=> + string(1) "A" } From 570b27331c6d684e5e852e38bdf9da0059b7f3e4 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 11:22:04 +0800 Subject: [PATCH 6/8] Cover wrapped empty generators in the regression --- ext/spl/tests/gh11591_3.phpt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ext/spl/tests/gh11591_3.phpt b/ext/spl/tests/gh11591_3.phpt index 660c88e2bd24..732710b75d7f 100644 --- a/ext/spl/tests/gh11591_3.phpt +++ b/ext/spl/tests/gh11591_3.phpt @@ -49,6 +49,13 @@ $iterator->append(new ArrayIterator(['A'])); $iterator->append(emptyGenerator()); values($iterator); values($iterator); + +echo "empty generator behind wrapper\n"; +$iterator = new AppendIterator(); +$iterator->append(new IteratorIterator(emptyGenerator())); +$iterator->append(new ArrayIterator(['A'])); +values($iterator); +values($iterator); ?> --EXPECT-- reinsert same key @@ -71,3 +78,12 @@ array(1) { [0]=> string(1) "A" } +empty generator behind wrapper +array(1) { + [0]=> + string(1) "A" +} +array(1) { + [0]=> + string(1) "A" +} From a54085f51615e1fbdf05b7a68ae8df4632515b83 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 12:17:37 +0800 Subject: [PATCH 7/8] Move the GH-11591 NEWS entry to the current version section --- NEWS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 704abf662454..2300def47290 100644 --- a/NEWS +++ b/NEWS @@ -32,6 +32,9 @@ PHP NEWS . Fixed bug GH-22818 (stream_filter_register() orphaned user_filter_map on shutdown re-registration). (David Carlier) +- SPL: + . Fixed bug GH-11591 (AppendIterator fails with an empty generator). + (Matthias Görgens) 30 Jul 2026, PHP 8.6.0alpha3 - Core: @@ -519,9 +522,6 @@ PHP NEWS . pwhash argument-validation errors now throw ValueError instead of SodiumException. (iliaal) -- SPL: - . Fixed bug GH-11591 (AppendIterator fails with an empty generator). - (Matthias Görgens) . DirectoryIterator key can now work better with filesystem supporting larger directory indexing. (David Carlier) . Fixed bug GH-21831 (SplObjectStorage::removeAllExcept() use-after-free with From fb7398bbb9a92cf83399f739e9dd939b1688ff19 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Fri, 14 Aug 2026 13:08:37 +0800 Subject: [PATCH 8/8] Drop the dead current_key field and cover throwing-generator parity --- ext/spl/spl_iterators.c | 12 +----------- ext/spl/tests/gh11591_2.phpt | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c index 00cec508ac1b..f4d31eed971d 100644 --- a/ext/spl/spl_iterators.c +++ b/ext/spl/spl_iterators.c @@ -121,7 +121,6 @@ typedef struct _spl_dual_it_object { } caching; struct { zval zarrayit; - zval current_key; zend_object_iterator *iterator; HashTable *empty_generators; } append; @@ -2031,9 +2030,7 @@ static void spl_dual_it_free_storage(zend_object *_object) if (object->u.append.empty_generators) { zend_weakrefs_hash_destroy(object->u.append.empty_generators); FREE_HASHTABLE(object->u.append.empty_generators); - } - if (!Z_ISUNDEF(object->u.append.current_key)) { - zval_ptr_dtor(&object->u.append.current_key); + object->u.append.empty_generators = NULL; } if (Z_TYPE(object->u.append.zarrayit) != IS_UNDEF) { zval_ptr_dtor(&object->u.append.zarrayit); @@ -2778,10 +2775,6 @@ PHP_METHOD(EmptyIterator, next) static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{{*/ { spl_dual_it_free(intern); - if (!Z_ISUNDEF(intern->u.append.current_key)) { - zval_ptr_dtor(&intern->u.append.current_key); - ZVAL_UNDEF(&intern->u.append.current_key); - } if (!Z_ISUNDEF(intern->inner.zobject)) { zval_ptr_dtor(&intern->inner.zobject); @@ -2798,8 +2791,6 @@ static zend_result spl_append_it_next_iterator(spl_dual_it_object *intern) /* {{ it = intern->u.append.iterator->funcs->get_current_data(intern->u.append.iterator); ZVAL_COPY(&intern->inner.zobject, it); intern->inner.ce = Z_OBJCE_P(it); - intern->u.append.iterator->funcs->get_current_key( - intern->u.append.iterator, &intern->u.append.current_key); if (intern->u.append.empty_generators) { zval *dummy = zend_hash_index_find(intern->u.append.empty_generators, zend_object_to_weakref_key(Z_OBJ_P(it))); @@ -2882,7 +2873,6 @@ PHP_METHOD(AppendIterator, __construct) } intern->dit_type = DIT_AppendIterator; - ZVAL_UNDEF(&intern->u.append.current_key); intern->u.append.empty_generators = NULL; object_init_with_constructor(&intern->u.append.zarrayit, spl_ce_ArrayIterator, 0, NULL, NULL); intern->u.append.iterator = spl_ce_ArrayIterator->get_iterator(spl_ce_ArrayIterator, &intern->u.append.zarrayit, 0); diff --git a/ext/spl/tests/gh11591_2.phpt b/ext/spl/tests/gh11591_2.phpt index 9c8c5b245cca..5d1630d62cd9 100644 --- a/ext/spl/tests/gh11591_2.phpt +++ b/ext/spl/tests/gh11591_2.phpt @@ -118,6 +118,19 @@ $generator = (function () use (&$iterator, &$generator): Generator { })(); $iterator->append($generator); values($iterator); + +echo "throwing\n"; +$iterator = new AppendIterator(); +try { + $iterator->append((function (): Generator { + if (false) { + yield; + } + throw new RuntimeException('boom'); + })()); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} ?> --EXPECT-- duplicate @@ -157,3 +170,5 @@ array(0) { reentrant move array(0) { } +throwing +boom