Skip to content

Fix bug #81156 unset() on ArrayObject inside foreach skips next index. #7553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,25 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
}
}

static zend_always_inline void zend_hash_set_is_delete(HashTable *ht, HashPosition from, HashPosition to)
{
if (UNEXPECTED(HT_HAS_ITERATORS(ht))) {
HashTableIterator *iter = EG(ht_iterators);
HashTableIterator *end = iter + EG(ht_iterators_used);

while (iter != end) {
if (iter->ht == ht) {
if (iter->pos == from) {
iter->is_delete = 1;
} else {
iter->is_delete = 0;
}
}
iter++;
}
}
}

static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx, Bucket *p, Bucket *prev)
{
if (!(HT_FLAGS(ht) & HASH_FLAG_PACKED)) {
Expand All @@ -1336,6 +1355,9 @@ static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx,
if (ht->nInternalPointer == idx) {
ht->nInternalPointer = new_idx;
}
/* I don't want to use function zend_hash_iterators_update() to set iter->is_delete = 1
* because zend_hash_iterators_update() was used in many place and will cause some unknown errors.*/
zend_hash_set_is_delete(ht, idx, new_idx);
zend_hash_iterators_update(ht, idx, new_idx);
}
if (ht->nNumUsed - 1 == idx) {
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ typedef uint32_t HashPosition;
typedef struct _HashTableIterator {
HashTable *ht;
HashPosition pos;
unsigned is_delete:1;
} HashTableIterator;

struct _zend_object {
Expand Down
32 changes: 31 additions & 1 deletion ext/spl/spl_array.c
Original file line number Diff line number Diff line change
Expand Up @@ -957,11 +957,41 @@ static void spl_array_it_get_current_key(zend_object_iterator *iter, zval *key)
}
/* }}} */

static int spl_array_check_if_delete(HashTable *ht) /* {{{ */
{
int result = FAILURE;
HashTableIterator *iter = EG(ht_iterators);
HashTableIterator *end = iter + EG(ht_iterators_used);

while (iter != end) {
/* The iterator has unset value in foreach, so the current position is valid. */
if (iter->ht == ht && iter->is_delete == 1) {
result = SUCCESS;
iter->is_delete = 0;
}
iter++;
}

return result;
}
/* }}} */

static void spl_array_it_move_forward(zend_object_iterator *iter) /* {{{ */
{
spl_array_object *object = Z_SPLARRAY_P(&iter->data);
HashTable *aht = spl_array_get_hash_table(object);
spl_array_next_ex(object, aht);

if (spl_array_check_if_delete(aht) != SUCCESS) {
spl_array_next_ex(object, aht);
} else {
/* execute the common part */
if (spl_array_is_object(object)) {
spl_array_skip_protected(object, aht);
} else {
uint32_t *pos_ptr = spl_array_get_pos_ptr(aht, object);
zend_hash_has_more_elements_ex(aht, pos_ptr);
}
}
}
/* }}} */

Expand Down
5 changes: 2 additions & 3 deletions ext/spl/tests/array_015.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ object(ArrayObject)#%d (1) {
}
}
1=>2
3=>4
object(ArrayObject)#%d (1) {
%s"storage"%s"ArrayObject":private]=>
array(1) {
[3]=>
int(4)
array(0) {
}
}
47 changes: 47 additions & 0 deletions ext/spl/tests/bug81156.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
Bug #81156 unset() on ArrayObject inside foreach skips next index
--FILE--
<?php

$ao = new ArrayObject([true, false, false, true]);
foreach ($ao as $key => $value) {
echo $key, "\n";
if (!$value) {
$ao->offsetUnset($key);
}
}
echo json_encode($ao), "\n";

$ao = new ArrayObject(["a" => true, "b" => false, "c" => false, "d" => true]);
foreach ($ao as $key => $value) {
echo $key, "\n";
if (!$value) {
$ao->offsetUnset($key);
}
}
echo json_encode($ao), "\n";

$ao = new ArrayObject([true, false, false, true]);
foreach ($ao as $key => $value) {
echo $key, "\n";
$ao->offsetUnset($key);
}
echo json_encode($ao), "\n";

?>
--EXPECT--
0
1
2
3
{"0":true,"3":true}
a
b
c
d
{"a":true,"d":true}
0
1
2
3
{}