From 545956c4f430f510deb9ee79c9823fd2ac66e96b Mon Sep 17 00:00:00 2001 From: JoaoMarcos44 <87440198+JoaoMarcos44@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:47:07 -0300 Subject: [PATCH] Fix GH-25107: Preserve foreach iterators on array_unshift --- NEWS | 2 + ext/standard/array.c | 20 ++++++- ext/standard/tests/array/gh25107.phpt | 84 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/array/gh25107.phpt diff --git a/NEWS b/NEWS index 529f07daccb8..3cc7545b4b51 100644 --- a/NEWS +++ b/NEWS @@ -271,6 +271,8 @@ PHP NEWS properties). (Lazizbek Ergashev) - Standard: + . Fixed bug GH-25107 (array_unshift() skips elements during foreach by + reference when the array contains holes). (JoaoMarcos44) . Fixed memory leaks in checkdnsrr() and dns_check_record() on Windows. (Ilia Alshanetsky) . Fixed __unserialize() and __wakeup() being called for objects with a missing diff --git a/ext/standard/array.c b/ext/standard/array.c index c10ad47c093f..e690cde4a9eb 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3826,7 +3826,25 @@ PHP_FUNCTION(array_unshift) } ZEND_HASH_FOREACH_END(); if (UNEXPECTED(HT_HAS_ITERATORS(Z_ARRVAL_P(stack)))) { - zend_hash_iterators_advance(Z_ARRVAL_P(stack), argc); + HashTable *ht = Z_ARRVAL_P(stack); + + if (!HT_IS_WITHOUT_HOLES(ht)) { + uint32_t iter_pos = zend_hash_iterators_lower_pos(ht, 0); + uint32_t compacted_pos = 0; + size_t element_size = ZEND_HASH_ELEMENT_SIZE(ht); + + for (uint32_t idx = 0; idx < ht->nNumUsed; idx++) { + if (idx == iter_pos) { + zend_hash_iterators_update(ht, idx, compacted_pos); + iter_pos = zend_hash_iterators_lower_pos(ht, idx + 1); + } + if (Z_TYPE_P(ZEND_HASH_ELEMENT_EX(ht, idx, element_size)) != IS_UNDEF) { + compacted_pos++; + } + } + zend_hash_iterators_update(ht, ht->nNumUsed, compacted_pos); + } + zend_hash_iterators_advance(ht, argc); HT_SET_ITERATORS_COUNT(&new_hash, HT_ITERATORS_COUNT(Z_ARRVAL_P(stack))); HT_SET_ITERATORS_COUNT(Z_ARRVAL_P(stack), 0); } diff --git a/ext/standard/tests/array/gh25107.phpt b/ext/standard/tests/array/gh25107.phpt new file mode 100644 index 000000000000..5dc34d051a61 --- /dev/null +++ b/ext/standard/tests/array/gh25107.phpt @@ -0,0 +1,84 @@ +--TEST-- +GH-25107 (array_unshift() skips values during reference iteration) +--FILE-- + 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e'] + : ['a', 'b', 'c', 'd', 'e']; + if ($mode !== 'dense') { + unset($a[$mixed ? 'a' : 0]); + } + $seen = []; + foreach ($a as &$v) { + $seen[] = $v; + if ($mode === 'end' && $v === 'e') { + array_unshift($a, 'x'); + $a[] = 'after'; + } elseif ($mode !== 'end' && $v === 'b') { + if ($mode === 'next-hole') { + unset($a[$mixed ? 'c' : 2]); + } + if ($mode === 'zero') { + array_unshift($a); + } elseif ($mode === 'many') { + array_unshift($a, 'x', 'y', 'z'); + } else { + array_unshift($a, 'x'); + } + } + } + unset($v); + echo ($mixed ? 'mixed' : 'packed'), ' ', $mode, ' ', json_encode($seen), "\n"; +} + +function nested(bool $mixed, bool $samePosition): void { + $a = $mixed + ? ['a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e', 'f' => 'f'] + : ['a', 'b', 'c', 'd', 'e', 'f']; + unset($a[$mixed ? 'a' : 0], $a[$mixed ? 'd' : 3]); + $outerSeen = []; + $innerSeen = []; + foreach ($a as &$outer) { + $outerSeen[] = $outer; + if ($outer !== 'b') { + continue; + } + foreach ($a as &$inner) { + $innerSeen[] = $inner; + if ($inner === ($samePosition ? 'b' : 'c')) { + array_unshift($a, 'x', 'y'); + } + } + unset($inner); + } + unset($outer); + echo ($mixed ? 'mixed' : 'packed'), ' ', ($samePosition ? 'same' : 'different'), + ' ', json_encode($outerSeen), ' ', json_encode($innerSeen), "\n"; +} + +foreach ([false, true] as $mixed) { + foreach (['dense', 'one', 'many', 'zero', 'next-hole', 'end'] as $mode) { + single($mixed, $mode); + } + nested($mixed, false); + nested($mixed, true); +} +?> +--EXPECT-- +packed dense ["a","b","c","d","e"] +packed one ["b","c","d","e"] +packed many ["b","c","d","e"] +packed zero ["b","c","d","e"] +packed next-hole ["b","d","e"] +packed end ["b","c","d","e","after"] +packed different ["b","c","e","f"] ["b","c","e","f"] +packed same ["b","c","e","f"] ["b","c","e","f"] +mixed dense ["a","b","c","d","e"] +mixed one ["b","c","d","e"] +mixed many ["b","c","d","e"] +mixed zero ["b","c","d","e"] +mixed next-hole ["b","d","e"] +mixed end ["b","c","d","e","after"] +mixed different ["b","c","e","f"] ["b","c","e","f"] +mixed same ["b","c","e","f"] ["b","c","e","f"]