From 47928572007284066ea10c7c0e2ad69b084debff Mon Sep 17 00:00:00 2001 From: Dmitry Rantovov Date: Tue, 8 Sep 2026 21:14:32 +0300 Subject: [PATCH] Fix GH-23617: Keep a past-the-end internal pointer past the end on compaction zend_array_dup() reset an exhausted internal pointer to 0, and both it and zend_hash_rehash() left it at the old nNumUsed when holes were compacted, so a later append was either skipped or the pointer jumped back to the first element. Move it to the new end instead, the same way past-the-end iterators are already migrated. Signed-off-by: Dmitry Rantovov Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Puqi4fj3kWhLHanMyF1UNU --- Zend/tests/gh23617.phpt | 37 +++++++++++++++++++++++++++++++++++++ Zend/zend_hash.c | 15 +++++++++------ 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 Zend/tests/gh23617.phpt diff --git a/Zend/tests/gh23617.phpt b/Zend/tests/gh23617.phpt new file mode 100644 index 000000000000..697336ec6d54 --- /dev/null +++ b/Zend/tests/gh23617.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-23617: Past-the-end internal pointer picks up an append after separation and compaction +--FILE-- + function () { $a = ['a' => 1, 'b' => 2, 'c' => 3]; $a['c'] = 3; return $a; }, + 'mixed with holes' => function () { $a = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5]; unset($a['b'], $a['c']); return $a; }, + 'full mixed with holes' => function () { $a = []; for ($i = 1; $i <= 8; $i++) $a["k$i"] = $i; unset($a['k2'], $a['k4'], $a['k6']); return $a; }, + 'packed' => function () { $a = [1, 2, 3]; $a[] = 4; return $a; }, + 'packed with holes' => function () { $a = [1, 2, 3, 4]; unset($a[1]); return $a; }, +]; +foreach ($makers as $name => $make) { + foreach ([false, true] as $separate) { + $a = $make(); + end($a); + next($a); + if ($separate) { + $copy = $a; + } + $a[] = 'new'; + echo $name, $separate ? ' (separated): ' : ': '; + var_dump(current($a)); + unset($copy); + } +} +?> +--EXPECT-- +mixed: string(3) "new" +mixed (separated): string(3) "new" +mixed with holes: string(3) "new" +mixed with holes (separated): string(3) "new" +full mixed with holes: string(3) "new" +full mixed with holes (separated): string(3) "new" +packed: string(3) "new" +packed (separated): string(3) "new" +packed with holes: string(3) "new" +packed with holes (separated): string(3) "new" diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 4640846ecc76..913b1ffa588e 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -1438,6 +1438,9 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht) /* Migrate pointer to one past the end of the array to the new one past the end, so that * newly inserted elements are picked up correctly. */ + if (ht->nInternalPointer >= old_num_used) { + ht->nInternalPointer = ht->nNumUsed; + } if (UNEXPECTED(HT_HAS_ITERATORS(ht))) { _zend_hash_iterators_update(ht, old_num_used, ht->nNumUsed); } @@ -2445,6 +2448,10 @@ static zend_always_inline uint32_t zend_array_dup_elements(const HashTable *sour /* Move past-the-end iterators so they can pick up newly appended elements. */ _zend_hash_iterators_update(target, source->nNumUsed, target_idx); } + /* Same for a past-the-end internal pointer. */ + if (target->nInternalPointer >= source->nNumUsed) { + target->nInternalPointer = target_idx; + } return target_idx; } idx++; p++; q++; @@ -2500,9 +2507,7 @@ ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(const HashTable *source) target->nNextFreeElement = source->nNextFreeElement; target->nTableSize = source->nTableSize; HT_SET_DATA_ADDR(target, emalloc(HT_PACKED_SIZE_EX(target->nTableSize, HT_MIN_MASK))); - target->nInternalPointer = - (source->nInternalPointer < source->nNumUsed) ? - source->nInternalPointer : 0; + target->nInternalPointer = source->nInternalPointer; HT_HASH_RESET_PACKED(target); @@ -2516,9 +2521,7 @@ ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(const HashTable *source) HT_FLAGS(target) = HT_FLAGS(source) & (HASH_FLAG_MASK & ~HASH_FLAG_HAS_EMPTY_IND); target->nTableMask = source->nTableMask; target->nNextFreeElement = source->nNextFreeElement; - target->nInternalPointer = - (source->nInternalPointer < source->nNumUsed) ? - source->nInternalPointer : 0; + target->nInternalPointer = source->nInternalPointer; target->nTableSize = source->nTableSize; HT_SET_DATA_ADDR(target, emalloc(HT_SIZE(target)));