Skip to content

Commit f47cdad

Browse files
committed
Merge branch 'PHP-8.5'
* PHP-8.5: [skip ci] Fix NEWS entry for #23607 Zend: Fix iterator relocation at the current element during rehash (#23607)
2 parents 0beb2a9 + 7a4c627 commit f47cdad

6 files changed

Lines changed: 156 additions & 4 deletions

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.6.0RC1
44

5+
- Core:
6+
. Fixed incorrect internal pointer and foreach iterator positions when
7+
compacting arrays with holes. (Weilin Du)
8+
59
- DOM:
610
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
711
registrations are freed while still reachable from the cycle collector.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Array duplication relocates an internal pointer on a hole, with and without foreach iterators
3+
--FILE--
4+
<?php
5+
function duplicate(array &$values): void {
6+
$copy = $values;
7+
$values['i'] = 18;
8+
echo 'current: ', key($values), '=>', current($values), "\n";
9+
next($values);
10+
echo 'next: ', key($values), '=>', current($values), "\n";
11+
echo 'copy current: ', key($copy), '=>', current($copy), "\n";
12+
echo 'copy keys: ', implode(' ', array_keys($copy)), "\n";
13+
}
14+
15+
foreach ([false, true] as $withIterator) {
16+
echo $withIterator ? "With iterator:\n" : "Without iterator:\n";
17+
$values = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
18+
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17];
19+
next($values);
20+
next($values);
21+
// Leave the internal pointer on a hole before several surviving elements.
22+
unset($values['a'], $values['b'], $values['c'], $values['d']);
23+
24+
if ($withIterator) {
25+
foreach ($values as &$value) {
26+
duplicate($values);
27+
break;
28+
}
29+
unset($value);
30+
} else {
31+
duplicate($values);
32+
}
33+
}
34+
?>
35+
--EXPECT--
36+
Without iterator:
37+
current: e=>14
38+
next: f=>15
39+
copy current: e=>14
40+
copy keys: e f g h
41+
With iterator:
42+
current: e=>14
43+
next: f=>15
44+
copy current: e=>14
45+
copy keys: e f g h
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Array duplication preserves past-the-end iterators when compacting holes
3+
--FILE--
4+
<?php
5+
$values = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13];
6+
unset($values['a'], $values['b']);
7+
8+
foreach ($values as $key => &$value) {
9+
echo "$key=>$value\n";
10+
if ($key === 'd') {
11+
// The iterator is one past the end; COW compacts the preceding holes.
12+
$copy = $values;
13+
$values['e'] = 14;
14+
}
15+
}
16+
unset($value);
17+
echo 'copy: ', implode(' ', array_keys($copy)), "\n";
18+
?>
19+
--EXPECT--
20+
c=>12
21+
d=>13
22+
e=>14
23+
copy: c d
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Array duplication updates iterators at both a hole and the next defined element
3+
--FILE--
4+
<?php
5+
function test(array $values): void {
6+
$outerVisits = [];
7+
$innerVisits = [];
8+
$first = true;
9+
foreach ($values as $outerKey => &$outerValue) {
10+
$outerVisits[] = "$outerKey=>$outerValue";
11+
if ($first) {
12+
$first = false;
13+
foreach ($values as $innerKey => &$innerValue) {
14+
$innerVisits[] = "$innerKey=>$innerValue";
15+
if ($innerValue === 11) {
16+
// The outer cursor is at a hole, the inner at the next value.
17+
unset($values['a'], $values['b']);
18+
$copy = $values;
19+
// Trigger copy-on-write duplication, which compacts the holes.
20+
$values['i'] = 18;
21+
}
22+
}
23+
unset($innerValue);
24+
}
25+
}
26+
unset($outerValue);
27+
echo 'outer: ', implode(' ', $outerVisits), "\n";
28+
echo 'inner: ', implode(' ', $innerVisits), "\n";
29+
echo 'copy: ', implode(' ', array_keys($copy)), "\n";
30+
}
31+
32+
test(['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
33+
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17]);
34+
?>
35+
--EXPECT--
36+
outer: a=>10 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
37+
inner: a=>10 b=>11 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
38+
copy: c d e f g h
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Rehashing updates iterators at both a hole and the next defined element
3+
--FILE--
4+
<?php
5+
function test(array $values, $firstKey, $secondKey, $newKey, int $newValue): void {
6+
$outerVisits = [];
7+
$innerVisits = [];
8+
$first = true;
9+
foreach ($values as $outerKey => &$outerValue) {
10+
$outerVisits[] = "$outerKey=>$outerValue";
11+
if ($first) {
12+
$first = false;
13+
foreach ($values as $innerKey => &$innerValue) {
14+
$innerVisits[] = "$innerKey=>$innerValue";
15+
if ($innerValue === 11) {
16+
// The outer cursor is at a hole, the inner at the next value.
17+
unset($values[$firstKey], $values[$secondKey]);
18+
$values[$newKey] = $newValue;
19+
}
20+
}
21+
unset($innerValue);
22+
}
23+
}
24+
unset($outerValue);
25+
echo 'outer: ', implode(' ', $outerVisits), "\n";
26+
echo 'inner: ', implode(' ', $innerVisits), "\n";
27+
}
28+
29+
// Adding a string key converts packed storage and compacts its holes.
30+
test([10, 11, 12, 13, 14], 0, 1, 'new', 15);
31+
32+
// Inserting into a full mixed table compacts its holes without growing it.
33+
test(['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
34+
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17], 'a', 'b', 'i', 18);
35+
?>
36+
--EXPECT--
37+
outer: 0=>10 2=>12 3=>13 4=>14 new=>15
38+
inner: 0=>10 1=>11 2=>12 3=>13 4=>14 new=>15
39+
outer: a=>10 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
40+
inner: a=>10 b=>11 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18

Zend/zend_hash.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,7 +1420,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
14201420
do {
14211421
zend_hash_iterators_update(ht, iter_pos, j);
14221422
iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1);
1423-
} while (iter_pos < i);
1423+
} while (iter_pos <= i);
14241424
}
14251425
q++;
14261426
j++;
@@ -2416,7 +2416,7 @@ static zend_always_inline uint32_t zend_array_dup_elements(const HashTable *sour
24162416
if (EXPECTED(!HT_HAS_ITERATORS(target))) {
24172417
while (p != end) {
24182418
if (zend_array_dup_element(source, target, target_idx, p, q, false, static_keys, with_holes)) {
2419-
if (source->nInternalPointer == idx) {
2419+
if (UNEXPECTED(target->nInternalPointer > target_idx && target->nInternalPointer <= idx)) {
24202420
target->nInternalPointer = target_idx;
24212421
}
24222422
target_idx++; q++;
@@ -2429,19 +2429,21 @@ static zend_always_inline uint32_t zend_array_dup_elements(const HashTable *sour
24292429

24302430
while (p != end) {
24312431
if (zend_array_dup_element(source, target, target_idx, p, q, false, static_keys, with_holes)) {
2432-
if (source->nInternalPointer == idx) {
2432+
if (UNEXPECTED(target->nInternalPointer > target_idx && target->nInternalPointer <= idx)) {
24332433
target->nInternalPointer = target_idx;
24342434
}
24352435
if (UNEXPECTED(idx >= iter_pos)) {
24362436
do {
24372437
zend_hash_iterators_update(target, iter_pos, target_idx);
24382438
iter_pos = zend_hash_iterators_lower_pos(target, iter_pos + 1);
2439-
} while (iter_pos < idx);
2439+
} while (iter_pos <= idx);
24402440
}
24412441
target_idx++; q++;
24422442
}
24432443
idx++; p++;
24442444
}
2445+
/* Move past-the-end iterators so they can pick up newly appended elements. */
2446+
_zend_hash_iterators_update(target, source->nNumUsed, target_idx);
24452447
}
24462448
return target_idx;
24472449
}

0 commit comments

Comments
 (0)