diff --git a/NEWS b/NEWS index 529f07daccb8..91cccc22317c 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, arshidkv12) . 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/Zend/zend_hash.c b/Zend/zend_hash.c index 3d88d179f7cc..8b7896312880 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -886,11 +886,28 @@ ZEND_API void ZEND_FASTCALL _zend_hash_iterators_update(HashTable *ht, HashPosit ZEND_API void ZEND_FASTCALL zend_hash_iterators_advance(HashTable *ht, HashPosition step) { HashTableIterator *iter = EG(ht_iterators); - HashTableIterator *end = iter + EG(ht_iterators_used); + HashTableIterator *end = iter + EG(ht_iterators_used); while (iter != end) { if (iter->ht == ht) { - iter->pos += step; + HashPosition pos = iter->pos; + HashPosition compacted_pos = 0; + + for (HashPosition idx = 0; idx < pos && idx < ht->nNumUsed; idx++) { + zval *val = HT_IS_PACKED(ht) + ? &ht->arPacked[idx] + : &ht->arData[idx].val; + + if (Z_TYPE_P(val) != IS_UNDEF) { + compacted_pos++; + } + } + + if (pos > ht->nNumUsed) { + compacted_pos += pos - ht->nNumUsed; + } + + iter->pos = compacted_pos + step; } iter++; } 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"]