Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ PHP NEWS
- Core:
. Fixed out-of-bounds reads during automatic UTF-16/32 encoding detection.
(Yudai Takada)
. Fixed incorrect internal pointer and foreach iterator positions when
compacting arrays with holes. (Weilin Du)
. Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or
next() call on the inner generator). (iliaal)
. Fixed bug GH-23232 (lone namespace separator asks the autoloader for an
Expand Down
45 changes: 45 additions & 0 deletions Zend/tests/array_dup_internal_pointer_hole.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
Array duplication relocates an internal pointer on a hole, with and without foreach iterators
--FILE--
<?php
function duplicate(array &$values): void {
$copy = $values;
$values['i'] = 18;
echo 'current: ', key($values), '=>', current($values), "\n";
next($values);
echo 'next: ', key($values), '=>', current($values), "\n";
echo 'copy current: ', key($copy), '=>', current($copy), "\n";
echo 'copy keys: ', implode(' ', array_keys($copy)), "\n";
}

foreach ([false, true] as $withIterator) {
echo $withIterator ? "With iterator:\n" : "Without iterator:\n";
$values = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17];
next($values);
next($values);
// Leave the internal pointer on a hole before several surviving elements.
unset($values['a'], $values['b'], $values['c'], $values['d']);

if ($withIterator) {
foreach ($values as &$value) {
duplicate($values);
break;
}
unset($value);
} else {
duplicate($values);
}
}
?>
--EXPECT--
Without iterator:
current: e=>14
next: f=>15
copy current: e=>14
copy keys: e f g h
With iterator:
current: e=>14
next: f=>15
copy current: e=>14
copy keys: e f g h
23 changes: 23 additions & 0 deletions Zend/tests/array_dup_iterator_past_end.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Array duplication preserves past-the-end iterators when compacting holes
--FILE--
<?php
$values = ['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13];
unset($values['a'], $values['b']);

foreach ($values as $key => &$value) {
echo "$key=>$value\n";
if ($key === 'd') {
// The iterator is one past the end; COW compacts the preceding holes.
$copy = $values;
$values['e'] = 14;
}
}
unset($value);
echo 'copy: ', implode(' ', array_keys($copy)), "\n";
?>
--EXPECT--
c=>12
d=>13
e=>14
copy: c d
38 changes: 38 additions & 0 deletions Zend/tests/array_dup_multiple_iterators.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
Array duplication updates iterators at both a hole and the next defined element
--FILE--
<?php
function test(array $values): void {
$outerVisits = [];
$innerVisits = [];
$first = true;
foreach ($values as $outerKey => &$outerValue) {
$outerVisits[] = "$outerKey=>$outerValue";
if ($first) {
$first = false;
foreach ($values as $innerKey => &$innerValue) {
$innerVisits[] = "$innerKey=>$innerValue";
if ($innerValue === 11) {
// The outer cursor is at a hole, the inner at the next value.
unset($values['a'], $values['b']);
$copy = $values;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$copy is write-only and is the only reason this write separates through zend_array_dup_elements() instead of zend_hash_rehash(). Delete it and the test still passes, but it becomes a duplicate of rehash_multiple_iterators.phpt case 2 and :2431 loses its only pin.

echo 'copy: ', implode(' ', array_keys($copy)), "\n"; with copy: c d e f g h in --EXPECT-- makes the line undeletable. Optional, nothing is wrong today.

// Trigger copy-on-write duplication, which compacts the holes.
$values['i'] = 18;
}
}
unset($innerValue);
}
}
unset($outerValue);
echo 'outer: ', implode(' ', $outerVisits), "\n";
echo 'inner: ', implode(' ', $innerVisits), "\n";
echo 'copy: ', implode(' ', array_keys($copy)), "\n";
}

test(['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17]);
?>
--EXPECT--
outer: a=>10 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
inner: a=>10 b=>11 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
copy: c d e f g h
40 changes: 40 additions & 0 deletions Zend/tests/rehash_multiple_iterators.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Rehashing updates iterators at both a hole and the next defined element
--FILE--
<?php
function test(array $values, $firstKey, $secondKey, $newKey, int $newValue): void {
$outerVisits = [];
$innerVisits = [];
$first = true;
foreach ($values as $outerKey => &$outerValue) {
$outerVisits[] = "$outerKey=>$outerValue";
if ($first) {
$first = false;
foreach ($values as $innerKey => &$innerValue) {
$innerVisits[] = "$innerKey=>$innerValue";
if ($innerValue === 11) {
// The outer cursor is at a hole, the inner at the next value.
unset($values[$firstKey], $values[$secondKey]);
$values[$newKey] = $newValue;
}
}
unset($innerValue);
}
}
unset($outerValue);
echo 'outer: ', implode(' ', $outerVisits), "\n";
echo 'inner: ', implode(' ', $innerVisits), "\n";
}

// Adding a string key converts packed storage and compacts its holes.
test([10, 11, 12, 13, 14], 0, 1, 'new', 15);

// Inserting into a full mixed table compacts its holes without growing it.
test(['a' => 10, 'b' => 11, 'c' => 12, 'd' => 13,
'e' => 14, 'f' => 15, 'g' => 16, 'h' => 17], 'a', 'b', 'i', 18);
?>
--EXPECT--
outer: 0=>10 2=>12 3=>13 4=>14 new=>15
inner: 0=>10 1=>11 2=>12 3=>13 4=>14 new=>15
outer: a=>10 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
inner: a=>10 b=>11 c=>12 d=>13 e=>14 f=>15 g=>16 h=>17 i=>18
10 changes: 6 additions & 4 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
do {
zend_hash_iterators_update(ht, iter_pos, j);
iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1);
} while (iter_pos < i);
} while (iter_pos <= i);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: while at it, you can also fix zend_array_dup_elements()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. and I think this is the similar fix to the existing one so we don't need to add more NEWS entry.

}
q++;
j++;
Expand Down Expand Up @@ -2408,7 +2408,7 @@ static zend_always_inline uint32_t zend_array_dup_elements(HashTable *source, Ha
if (EXPECTED(!HT_HAS_ITERATORS(target))) {
while (p != end) {
if (zend_array_dup_element(source, target, target_idx, p, q, 0, static_keys, with_holes)) {
if (source->nInternalPointer == idx) {
if (UNEXPECTED(target->nInternalPointer > target_idx && target->nInternalPointer <= idx)) {
target->nInternalPointer = target_idx;
}
target_idx++; q++;
Expand All @@ -2421,19 +2421,21 @@ static zend_always_inline uint32_t zend_array_dup_elements(HashTable *source, Ha

while (p != end) {
if (zend_array_dup_element(source, target, target_idx, p, q, 0, static_keys, with_holes)) {
if (source->nInternalPointer == idx) {
if (UNEXPECTED(target->nInternalPointer > target_idx && target->nInternalPointer <= idx)) {
target->nInternalPointer = target_idx;
}
if (UNEXPECTED(idx >= iter_pos)) {
do {
zend_hash_iterators_update(target, iter_pos, target_idx);
iter_pos = zend_hash_iterators_lower_pos(target, iter_pos + 1);
} while (iter_pos < idx);
} while (iter_pos <= idx);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more relocation gaps in this function, both pre-existing and both already handled in zend_hash_rehash():

  • No one-past-the-end migration. Rehash does it at :1434, and this loop cannot reach that position under either bound, since lower_pos() returns nNumUsed as both its sentinel and a real cursor there. A by-ref foreach on the last element then loses elements appended after a COW dup.
  • :2411 and :2424 still use source->nInternalPointer == idx, the form 5d40592 replaced in rehash with the (j, i] range at :1389. A cursor parked on a hole is never remapped, so key()/next() skips live elements. Test target-> rather than source->, at both sites.

Follow-up is fine, neither is yours.

}
target_idx++; q++;
}
idx++; p++;
}
/* Move past-the-end iterators so they can pick up newly appended elements. */
_zend_hash_iterators_update(target, source->nNumUsed, target_idx);
}
return target_idx;
}
Expand Down
Loading