From 47c7da9ecf5aef8a72268589807ec48e03ff21ec Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 12 Nov 2026 18:34:58 +0100 Subject: [PATCH] Fix GH-17935: Separate ArrayObject and ArrayIterator storage on write ArrayObject and ArrayIterator assumed they were the only owner of their storage, but __serialize(), __debugInfo(), var_export(), json_encode() and the sort methods hand it out or borrow it without copying it. Writing to the object then modified the array that was handed out, and failed the refcount assertion of zend_hash.c in debug builds. The storage is now separated before any write. When this happens while it is iterated, iterators move to the copy of their position that zend_array_dup() registers on the new table. When the storage is replaced, they keep their position and get registered on the new table. An error handler can now separate the storage by writing to the object, so the table is fetched after the code that can call one: get_hash_key() on read paths, and argument parsing in ArrayIterator::seek(). This also fixes use-after-frees when a handler calls exchangeArray() there, or while an element is being created. --- NEWS | 2 + ext/spl/spl_array.c | 94 ++++++++-- ...bject_exchange_array_in_error_handler.phpt | 21 +++ .../ArrayObject_separate_on_write.phpt | 82 +++++++++ ...bject_separate_on_write_error_handler.phpt | 52 ++++++ ...rayObject_separate_on_write_iterators.phpt | 164 ++++++++++++++++++ ...ArrayObject_separate_on_write_reentry.phpt | 63 +++++++ .../ArrayObject_separate_on_write_twice.phpt | 29 ++++ ...storage_changed_in_error_handler_read.phpt | 82 +++++++++ ext/spl/tests/ArrayObject/gh17935.phpt | 27 +++ 10 files changed, 605 insertions(+), 11 deletions(-) create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_exchange_array_in_error_handler.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_separate_on_write.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_error_handler.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_iterators.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_reentry.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_twice.phpt create mode 100644 ext/spl/tests/ArrayObject/ArrayObject_storage_changed_in_error_handler_read.phpt create mode 100644 ext/spl/tests/ArrayObject/gh17935.phpt diff --git a/NEWS b/NEWS index a0bce4ed6f43..b96f6f774919 100644 --- a/NEWS +++ b/NEWS @@ -152,6 +152,8 @@ PHP NEWS (Ilia Alshanetsky) - SPL: + . Fixed bug GH-17935 (Assertion failure Zend/zend_hash.c when appending to + serialized ArrayObject). (Nicolas Grekas) . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when __serialize() removes an element). (David Carlier) diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 9ca034e72abb..d6d0b219ea98 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -108,9 +108,22 @@ static inline bool spl_array_is_object(spl_array_object *intern) /* {{{ */ } /* }}} */ +/* Returns the storage, separated from its other holders so that it can be written to. */ +static HashTable *spl_array_get_hash_table_for_write(spl_array_object *intern) +{ + while (intern->ar_flags & SPL_ARRAY_USE_OTHER) { + intern = Z_SPLARRAY_P(&intern->array); + } + if (!(intern->ar_flags & SPL_ARRAY_IS_SELF) && Z_TYPE(intern->array) == IS_ARRAY) { + SEPARATE_ARRAY(&intern->array); + return Z_ARRVAL(intern->array); + } + return spl_array_get_hash_table(intern); +} + static zend_result spl_array_skip_protected(spl_array_object *intern, HashTable *aht); -static zend_never_inline void spl_array_create_ht_iter(HashTable *ht, spl_array_object* intern) /* {{{ */ +static void spl_array_create_ht_iter(HashTable *ht, spl_array_object* intern) /* {{{ */ { intern->ht_iter = zend_hash_iterator_add(ht, zend_hash_get_current_pos(ht)); zend_hash_internal_pointer_reset_ex(ht, &EG(ht_iterators)[intern->ht_iter].pos); @@ -118,10 +131,52 @@ static zend_never_inline void spl_array_create_ht_iter(HashTable *ht, spl_array_ } /* }}} */ -static zend_always_inline uint32_t *spl_array_get_pos_ptr(HashTable *ht, spl_array_object* intern) /* {{{ */ +static void spl_array_update_ht_iter(HashTable *ht, spl_array_object* intern) { - if (UNEXPECTED(intern->ht_iter == (uint32_t)-1)) { + uint32_t idx = intern->ht_iter; + HashPosition pos = EG(ht_iterators)[idx].pos; + bool has_copy = false; + + /* When the storage is separated, zend_array_dup() registers a copy of the + * iterator on the new table, with its position adjusted to it. */ + for (uint32_t i = EG(ht_iterators)[idx].next_copy; i != idx; i = EG(ht_iterators)[i].next_copy) { + if (EG(ht_iterators)[i].ht == ht) { + has_copy = true; + break; + } + } + + /* Moves the registration to the new table, so that the previous one stops + * tracking this iterator, and takes the position of the copy if there is one. */ + zend_hash_iterator_pos(idx, ht); + if (!has_copy) { + /* The storage was replaced: the position is kept as is */ + EG(ht_iterators)[idx].pos = pos; + } +} + +static zend_always_inline bool spl_array_ht_iter_is_on(const spl_array_object *intern, const HashTable *ht) +{ + /* The registered table is compared rather than a cached pointer: it is + * poisoned when the table is destroyed, so a new table allocated at the + * same address cannot be mistaken for it. */ + return intern->ht_iter != (uint32_t)-1 && EG(ht_iterators)[intern->ht_iter].ht == ht; +} + +static zend_never_inline uint32_t *spl_array_get_pos_ptr_slow(HashTable *ht, spl_array_object* intern) +{ + if (intern->ht_iter == (uint32_t)-1) { spl_array_create_ht_iter(ht, intern); + } else { + spl_array_update_ht_iter(ht, intern); + } + return &EG(ht_iterators)[intern->ht_iter].pos; +} + +static zend_always_inline uint32_t *spl_array_get_pos_ptr(HashTable *ht, spl_array_object* intern) /* {{{ */ +{ + if (UNEXPECTED(!spl_array_ht_iter_is_on(intern, ht))) { + return spl_array_get_pos_ptr_slow(ht, intern); } return &EG(ht_iterators)[intern->ht_iter].pos; } @@ -321,9 +376,9 @@ static zval *spl_array_get_dimension_ptr(bool check_inherited, spl_array_object { zval *retval; spl_hash_key key; - HashTable *ht = spl_array_get_hash_table(intern); + HashTable *ht; - if (!offset || Z_ISUNDEF_P(offset) || !ht) { + if (!offset || Z_ISUNDEF_P(offset)) { return &EG(uninitialized_zval); } @@ -338,6 +393,13 @@ static zval *spl_array_get_dimension_ptr(bool check_inherited, spl_array_object &EG(error_zval) : &EG(uninitialized_zval); } + /* The table is fetched after get_hash_key(), whose errors may call a handler + * that shares or replaces the storage. In write context, the returned zval + * may be turned into a reference or written to. */ + ht = (type != BP_VAR_R && type != BP_VAR_IS) + ? spl_array_get_hash_table_for_write(intern) + : spl_array_get_hash_table(intern); + if (key.key) { retval = zend_hash_find(ht, key.key); if (retval) { @@ -372,6 +434,8 @@ static zval *spl_array_get_dimension_ptr(bool check_inherited, spl_array_object break; case BP_VAR_RW: zend_error(E_WARNING,"Undefined array key \"%pS\"", key.key); + /* The error handler may have shared or replaced the storage */ + ht = spl_array_get_hash_table_for_write(intern); ZEND_FALLTHROUGH; case BP_VAR_W: { zval value; @@ -393,6 +457,8 @@ static zval *spl_array_get_dimension_ptr(bool check_inherited, spl_array_object break; case BP_VAR_RW: zend_error(E_WARNING, "Undefined array key " ZEND_LONG_FMT, key.h); + /* The error handler may have shared or replaced the storage */ + ht = spl_array_get_hash_table_for_write(intern); ZEND_FALLTHROUGH; case BP_VAR_W: { zval value; @@ -480,7 +546,7 @@ static void spl_array_write_dimension_ex(int check_inherited, zend_object *objec Z_TRY_ADDREF_P(value); if (!offset || Z_TYPE_P(offset) == IS_NULL) { - ht = spl_array_get_hash_table(intern); + ht = spl_array_get_hash_table_for_write(intern); if (UNEXPECTED(ht == intern->sentinel_array)) { return; } @@ -494,7 +560,7 @@ static void spl_array_write_dimension_ex(int check_inherited, zend_object *objec return; } - ht = spl_array_get_hash_table(intern); + ht = spl_array_get_hash_table_for_write(intern); if (UNEXPECTED(ht == intern->sentinel_array)) { spl_hash_key_release(&key); return; @@ -534,7 +600,7 @@ static void spl_array_unset_dimension_ex(int check_inherited, zend_object *objec return; } - ht = spl_array_get_hash_table(intern); + ht = spl_array_get_hash_table_for_write(intern); if (key.key) { zval *data = zend_hash_find(ht, key.key); if (data) { @@ -592,7 +658,7 @@ static bool spl_array_has_dimension_ex(bool check_inherited, zend_object *object } if (!value) { - HashTable *ht = spl_array_get_hash_table(intern); + HashTable *ht; spl_hash_key key; if (get_hash_key(&key, intern, offset) == FAILURE) { @@ -600,6 +666,9 @@ static bool spl_array_has_dimension_ex(bool check_inherited, zend_object *object return false; } + /* Fetched after get_hash_key(), whose errors may call a handler that replaces the storage */ + ht = spl_array_get_hash_table(intern); + if (key.key) { tmp = zend_hash_find(ht, key.key); spl_hash_key_release(&key); @@ -1562,7 +1631,9 @@ static zval *spl_array_it_get_current_data(zend_object_iterator *iter) /* {{{ */ { spl_array_iterator *array_iter = (spl_array_iterator*)iter; spl_array_object *object = Z_SPLARRAY_P(&iter->data); - HashTable *aht = spl_array_get_hash_table(object); + HashTable *aht = array_iter->by_ref + ? spl_array_get_hash_table_for_write(object) + : spl_array_get_hash_table(object); zval *data = zend_hash_get_current_data_ex(aht, spl_array_get_pos_ptr(aht, object)); if (data && Z_TYPE_P(data) == IS_INDIRECT) { data = Z_INDIRECT_P(data); @@ -1705,7 +1776,7 @@ PHP_METHOD(ArrayIterator, seek) zend_long opos, position; zval *object = ZEND_THIS; spl_array_object *intern = Z_SPLARRAY_P(object); - HashTable *aht = spl_array_get_hash_table(intern); + HashTable *aht; int result; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &position) == FAILURE) { @@ -1720,6 +1791,7 @@ PHP_METHOD(ArrayIterator, seek) while (position-- > 0 && (result = spl_array_next(intern)) == SUCCESS); + aht = spl_array_get_hash_table(intern); if (result == SUCCESS && zend_hash_has_more_elements_ex(aht, spl_array_get_pos_ptr(aht, intern)) == SUCCESS) { return; /* ok */ } diff --git a/ext/spl/tests/ArrayObject/ArrayObject_exchange_array_in_error_handler.phpt b/ext/spl/tests/ArrayObject/ArrayObject_exchange_array_in_error_handler.phpt new file mode 100644 index 000000000000..8f249c291899 --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_exchange_array_in_error_handler.phpt @@ -0,0 +1,21 @@ +--TEST-- +ArrayObject storage exchanged by an error handler while creating an element +--FILE-- + 1], ArrayObject::ARRAY_AS_PROPS); +set_error_handler(function ($errno, $errstr) use ($ao) { + echo $errstr, "\n"; + $ao->exchangeArray(['z' => str_repeat('z', 3)]); +}); +$ao->b .= 'x'; +restore_error_handler(); +var_dump($ao->getArrayCopy()); +?> +--EXPECT-- +Undefined array key "b" +array(2) { + ["z"]=> + string(3) "zzz" + ["b"]=> + string(1) "x" +} diff --git a/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write.phpt b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write.phpt new file mode 100644 index 000000000000..79fe9c1cbabe --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write.phpt @@ -0,0 +1,82 @@ +--TEST-- +ArrayObject and ArrayIterator separate their storage from the arrays they hand out before writing to it +--FILE-- + 1, 'b' => ['x' => 1], 'c' => 3]; + $object = new $class($expected, ArrayObject::ARRAY_AS_PROPS); + $serialized = $object->__serialize()[1]; + $debug = array_values($object->__debugInfo())[0]; + $write($object); + if ($serialized !== $expected || $debug !== $expected) { + echo "$class $label: FAIL\n"; + var_dump($serialized, $debug); + } + echo "$class $label: ", json_encode($object->getArrayCopy()), "\n"; + } +} + +check('offsetSet', function ($o) { $o['a'] = 2; }); +check('offsetSet()', function ($o) { $o->offsetSet('a', 2); }); +check('append', function ($o) { $o[] = 4; }); +check('append()', function ($o) { $o->append(4); }); +check('offsetUnset', function ($o) { unset($o['a']); }); +check('offsetUnset() missing', function ($o) { $o->offsetUnset('z'); }); +check('compound assign', function ($o) { $o['a'] += 10; }); +check('increment', function ($o) { $o['a']++; }); +check('nested write', function ($o) { $o['b']['y'] = 2; }); +check('nested append', function ($o) { $o['b'][] = 2; }); +check('nested unset', function ($o) { unset($o['b']['x']); }); +check('reference', function ($o) { $r = &$o['a']; $r = 5; }); +check('property', function ($o) { $o->a = 6; }); +check('property compound assign', function ($o) { $o->a .= 'x'; }); +check('property nested write', function ($o) { $o->b['z'] = 3; }); +check('property reference', function ($o) { $r = &$o->c; $r = 7; }); +check('property unset', function ($o) { unset($o->c); }); +check('foreach by reference', function ($o) { foreach ($o as &$v) { $v = 8; } }); +check('uasort', function ($o) { $o->uasort(fn ($a, $b) => is_array($a) <=> is_array($b)); }, ['ArrayObject']); +check('ksort', function ($o) { $o->ksort(); $o['d'] = 0; }, ['ArrayObject']); +?> +--EXPECT-- +ArrayIterator offsetSet: {"a":2,"b":{"x":1},"c":3} +ArrayObject offsetSet: {"a":2,"b":{"x":1},"c":3} +ArrayIterator offsetSet(): {"a":2,"b":{"x":1},"c":3} +ArrayObject offsetSet(): {"a":2,"b":{"x":1},"c":3} +ArrayIterator append: {"a":1,"b":{"x":1},"c":3,"0":4} +ArrayObject append: {"a":1,"b":{"x":1},"c":3,"0":4} +ArrayIterator append(): {"a":1,"b":{"x":1},"c":3,"0":4} +ArrayObject append(): {"a":1,"b":{"x":1},"c":3,"0":4} +ArrayIterator offsetUnset: {"b":{"x":1},"c":3} +ArrayObject offsetUnset: {"b":{"x":1},"c":3} +ArrayIterator offsetUnset() missing: {"a":1,"b":{"x":1},"c":3} +ArrayObject offsetUnset() missing: {"a":1,"b":{"x":1},"c":3} +ArrayIterator compound assign: {"a":11,"b":{"x":1},"c":3} +ArrayObject compound assign: {"a":11,"b":{"x":1},"c":3} +ArrayIterator increment: {"a":2,"b":{"x":1},"c":3} +ArrayObject increment: {"a":2,"b":{"x":1},"c":3} +ArrayIterator nested write: {"a":1,"b":{"x":1,"y":2},"c":3} +ArrayObject nested write: {"a":1,"b":{"x":1,"y":2},"c":3} +ArrayIterator nested append: {"a":1,"b":{"x":1,"0":2},"c":3} +ArrayObject nested append: {"a":1,"b":{"x":1,"0":2},"c":3} +ArrayIterator nested unset: {"a":1,"b":[],"c":3} +ArrayObject nested unset: {"a":1,"b":[],"c":3} +ArrayIterator reference: {"a":5,"b":{"x":1},"c":3} +ArrayObject reference: {"a":5,"b":{"x":1},"c":3} +ArrayIterator property: {"a":6,"b":{"x":1},"c":3} +ArrayObject property: {"a":6,"b":{"x":1},"c":3} +ArrayIterator property compound assign: {"a":"1x","b":{"x":1},"c":3} +ArrayObject property compound assign: {"a":"1x","b":{"x":1},"c":3} +ArrayIterator property nested write: {"a":1,"b":{"x":1,"z":3},"c":3} +ArrayObject property nested write: {"a":1,"b":{"x":1,"z":3},"c":3} +ArrayIterator property reference: {"a":1,"b":{"x":1},"c":7} +ArrayObject property reference: {"a":1,"b":{"x":1},"c":7} +ArrayIterator property unset: {"a":1,"b":{"x":1}} +ArrayObject property unset: {"a":1,"b":{"x":1}} +ArrayIterator foreach by reference: {"a":8,"b":8,"c":8} +ArrayObject foreach by reference: {"a":8,"b":8,"c":8} +ArrayObject uasort: {"a":1,"c":3,"b":{"x":1}} +ArrayObject ksort: {"a":1,"b":{"x":1},"c":3,"d":0} diff --git a/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_error_handler.phpt b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_error_handler.phpt new file mode 100644 index 000000000000..5144dda3d04b --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_error_handler.phpt @@ -0,0 +1,52 @@ +--TEST-- +ArrayObject storage shared by an error handler while creating an element +--FILE-- + 1], ArrayObject::ARRAY_AS_PROPS); +set_error_handler(function ($errno, $errstr) use ($ao) { + echo $errstr, "\n"; + $GLOBALS['copies'][] = $ao->__serialize()[1]; +}); +$ao->b .= 'x'; +$ao->c++; +$ao['d'] .= 'y'; +restore_error_handler(); +var_dump($ao->getArrayCopy(), $copies); +?> +--EXPECT-- +Undefined array key "b" +Undefined array key "c" +Undefined array key "d" +array(4) { + ["a"]=> + int(1) + ["b"]=> + string(1) "x" + ["c"]=> + int(1) + ["d"]=> + string(1) "y" +} +array(3) { + [0]=> + array(1) { + ["a"]=> + int(1) + } + [1]=> + array(2) { + ["a"]=> + int(1) + ["b"]=> + string(1) "x" + } + [2]=> + array(3) { + ["a"]=> + int(1) + ["b"]=> + string(1) "x" + ["c"]=> + int(1) + } +} diff --git a/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_iterators.phpt b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_iterators.phpt new file mode 100644 index 000000000000..5d0058974a8d --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_iterators.phpt @@ -0,0 +1,164 @@ +--TEST-- +ArrayObject and ArrayIterator keep their iteration position when their storage is separated +--FILE-- + 0, 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]); + unset($object['z']); + return $object; +} + +echo "-- write during foreach\n"; +$it = make('ArrayIterator'); +foreach ($it as $k => $v) { + echo "$k=$v "; + $data = $it->__serialize(); + $it[$k] = $v * 10; +} +echo "\n", json_encode($it->getArrayCopy()), ' ', json_encode($data[1]), "\n"; + +echo "-- write during manual iteration\n"; +$it = make('ArrayIterator'); +$it->rewind(); +$it->next(); +$data = $it->__serialize(); +$it['e'] = 5; +for (; $it->valid(); $it->next()) { + echo $it->key(), '=', $it->current(), ' '; +} +echo "\n", json_encode($data[1]), "\n"; + +echo "-- unset during foreach\n"; +$it = make('ArrayIterator'); +foreach ($it as $k => $v) { + echo "$k=$v "; + if ($k === 'b') { + $data = $it->__debugInfo(); + unset($it['c']); + } +} +echo "\n", json_encode($it->getArrayCopy()), ' ', json_encode(array_values($data)[0]), "\n"; + +echo "-- ArrayObject iterator, write through the ArrayObject\n"; +$ao = make('ArrayObject'); +foreach ($ao as $k => $v) { + echo "$k=$v "; + $data = $ao->__serialize(); + $ao[$k] = -$v; +} +echo "\n", json_encode($ao->getArrayCopy()), ' ', json_encode($data[1]), "\n"; + +echo "-- two ArrayObject iterators\n"; +$ao = make('ArrayObject'); +$it1 = $ao->getIterator(); +$it2 = $ao->getIterator(); +$it1->next(); +$it2->next(); +$it2->next(); +$data = $ao->__serialize(); +$ao['f'] = 6; +echo $it1->key(), ' ', $it2->key(), "\n"; +$it1->next(); +$it2->next(); +echo $it1->key(), ' ', $it2->key(), "\n"; + +echo "-- iterator created after the separation\n"; +$ao = make('ArrayObject'); +$it1 = $ao->getIterator(); +$it1->next(); +$data = $ao->__serialize(); +unset($ao['a']); +$it2 = $ao->getIterator(); +echo $it1->key(), ' ', $it2->key(), "\n"; +$data = $ao->__serialize(); +$ao['g'] = 7; +$it1->next(); +$it2->next(); +echo $it1->key(), ' ', $it2->key(), "\n"; + +echo "-- foreach by reference\n"; +$ao = make('ArrayObject'); +$data = $ao->__serialize(); +foreach ($ao as $k => &$v) { + echo "$k=$v "; + $v *= 2; +} +unset($v); +echo "\n", json_encode($ao->getArrayCopy()), ' ', json_encode($data[1]), "\n"; + +echo "-- nested foreach with writes\n"; +$ao = make('ArrayObject'); +foreach ($ao as $k1 => $v1) { + foreach ($ao as $k2 => $v2) { + if ($k2 === 'b') { + $data = $ao->__serialize(); + $ao['b'] = $v2 + 1; + } + } + echo "$k1=$v1 "; +} +echo "\n", json_encode($ao->getArrayCopy()), "\n"; + +echo "-- sort during foreach\n"; +$ao = make('ArrayObject'); +foreach ($ao as $k => $v) { + echo "$k=$v "; + if ($k === 'b') { + $ao->ksort(); + } +} +echo "\n"; + +echo "-- storage replaced during iteration\n"; +$ao = make('ArrayObject'); +$it = $ao->getIterator(); +$it->next(); +$it->next(); +$new = []; +foreach (['p', 'q', 'r', 's', 't'] as $k) { + $new[$k] = $k; +} +$ao->exchangeArray($new); +// The position is kept as is +echo $it->key(), "\n"; +// It then follows the new storage, here when filling it compacts it +unset($ao['p'], $ao['q']); +for ($i = 0; $i < 8; $i++) { + $ao["k$i"] = $i; +} +echo $it->key(), "\n"; +?> +--EXPECT-- +-- write during foreach +a=1 b=2 c=3 d=4 +{"a":10,"b":20,"c":30,"d":40} {"a":10,"b":20,"c":30,"d":4} +-- write during manual iteration +b=2 c=3 d=4 e=5 +{"a":1,"b":2,"c":3,"d":4} +-- unset during foreach +a=1 b=2 d=4 +{"a":1,"b":2,"d":4} {"a":1,"b":2,"c":3,"d":4} +-- ArrayObject iterator, write through the ArrayObject +a=1 b=2 c=3 d=4 +{"a":-1,"b":-2,"c":-3,"d":-4} {"a":-1,"b":-2,"c":-3,"d":4} +-- two ArrayObject iterators +b c +c d +-- iterator created after the separation +b b +c c +-- foreach by reference +a=1 b=2 c=3 d=4 +{"a":2,"b":4,"c":6,"d":8} {"a":1,"b":2,"c":3,"d":4} +-- nested foreach with writes +a=1 b=3 c=3 d=4 +{"a":1,"b":6,"c":3,"d":4} +-- sort during foreach +a=1 b=2 c=3 d=4 +-- storage replaced during iteration +s +s diff --git a/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_reentry.phpt b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_reentry.phpt new file mode 100644 index 000000000000..21ba4d484a6c --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_reentry.phpt @@ -0,0 +1,63 @@ +--TEST-- +ArrayObject and ArrayIterator written to while their storage is being encoded, sorted or compared +--FILE-- +target["new$i"] = $i; + } + unset($this->target['b']); + $ref = &$this->target['ref']; + $ref = 'ref'; + + return 'written'; + } + + public function __toString(): string + { + $this->target[] = 'appended'; + + return 'x'; + } +} + +echo "-- json_encode()\n"; +foreach (['ArrayObject', 'ArrayIterator'] as $class) { + $object = new $class(['a' => null, 'b' => 2, 'c' => 3]); + $object['a'] = new Writer($object); + echo json_encode($object), ' ', count($object), "\n"; +} + +echo "-- sort\n"; +$ao = new ArrayObject(['b' => 2, 'a' => 1, 'c' => 3]); +$it = $ao->getIterator(); +$ao->uasort(function ($x, $y) use ($it) { + // Written to a copy that the sorted array replaces + $it['d'] = 4; + unset($it['c']); + + return $x <=> $y; +}); +echo json_encode($ao->getArrayCopy()), "\n"; + +echo "-- comparison\n"; +$a = new ArrayObject(); +$a[] = new Writer($a); +$a[] = 'y'; +$b = new ArrayObject(['x', 'y']); +var_dump($a == $b, count($a)); +?> +--EXPECT-- +-- json_encode() +{"a":"written","b":2,"c":3} 23 +{"a":"written","b":2,"c":3} 23 +-- sort +{"a":1,"b":2,"c":3} +-- comparison +bool(true) +int(3) diff --git a/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_twice.phpt b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_twice.phpt new file mode 100644 index 000000000000..df3a405cd31b --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_separate_on_write_twice.phpt @@ -0,0 +1,29 @@ +--TEST-- +ArrayObject iterator keeps its position when the storage is separated twice between two steps +--SKIPIF-- + +--FILE-- + 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5]); +foreach ($ao as $k => $v) { + echo "$k "; + if ($k === 'b') { + // A hole before the current position, which a copy compacts + unset($ao['a']); + $data = $ao->__serialize(); + $ao['x'] = 1; + unset($data); + $data = $ao->__serialize(); + $ao['y'] = 1; + } +} +echo "\n"; +?> +--EXPECT-- +a b c d e x y diff --git a/ext/spl/tests/ArrayObject/ArrayObject_storage_changed_in_error_handler_read.phpt b/ext/spl/tests/ArrayObject/ArrayObject_storage_changed_in_error_handler_read.phpt new file mode 100644 index 000000000000..5f290eb80f87 --- /dev/null +++ b/ext/spl/tests/ArrayObject/ArrayObject_storage_changed_in_error_handler_read.phpt @@ -0,0 +1,82 @@ +--TEST-- +ArrayObject storage separated or replaced by an error handler while reading an element +--FILE-- + function ($ao) { + $ao->exchangeArray(['' => 'other empty', 1 => 'other one']); + }, + 'separated' => function ($ao) { + $data = $ao->__serialize(); + $ao['new'] = 1; + // Frees the table that the storage was separated from + unset($data); + }, +]; +$reads = [ + '$ao[null]' => fn ($ao) => $ao[null], + 'isset($ao[null])' => fn ($ao) => isset($ao[null]), + 'empty($ao[null])' => fn ($ao) => empty($ao[null]), + '$ao->offsetGet(null)' => fn ($ao) => $ao->offsetGet(null), + '$ao->offsetExists(null)' => fn ($ao) => $ao->offsetExists(null), + '$ao[1.5]' => fn ($ao) => $ao[1.5], + 'isset($ao[1.5])' => fn ($ao) => isset($ao[1.5]), + 'seek(1.5)' => function ($ao) { + $it = $ao->getIterator(); + $it->seek(1.5); + return $it->current(); + }, +]; + +foreach ($handlers as $how => $handler) { + echo "-- $how\n"; + foreach ($reads as $label => $read) { + $ao = new ArrayObject(['' => 'empty', 1 => 'one', 2 => 'two']); + set_error_handler(function ($errno, $errstr) use ($ao, $handler) { + echo $errstr, "\n"; + $handler($ao); + }); + $result = $read($ao); + restore_error_handler(); + echo $label, ': ', var_export($result, true), "\n"; + } +} +?> +--EXPECT-- +-- replaced +Using null as an array offset is deprecated, use an empty string instead +$ao[null]: 'other empty' +Using null as an array offset is deprecated, use an empty string instead +isset($ao[null]): true +Using null as an array offset is deprecated, use an empty string instead +empty($ao[null]): false +Using null as an array offset is deprecated, use an empty string instead +$ao->offsetGet(null): 'other empty' +Using null as an array offset is deprecated, use an empty string instead +$ao->offsetExists(null): true +Implicit conversion from float 1.5 to int loses precision +$ao[1.5]: 'other one' +Implicit conversion from float 1.5 to int loses precision +isset($ao[1.5]): true +Implicit conversion from float 1.5 to int loses precision +seek(1.5): 'other one' +-- separated +Using null as an array offset is deprecated, use an empty string instead +$ao[null]: 'empty' +Using null as an array offset is deprecated, use an empty string instead +isset($ao[null]): true +Using null as an array offset is deprecated, use an empty string instead +empty($ao[null]): false +Using null as an array offset is deprecated, use an empty string instead +$ao->offsetGet(null): 'empty' +Using null as an array offset is deprecated, use an empty string instead +$ao->offsetExists(null): true +Implicit conversion from float 1.5 to int loses precision +$ao[1.5]: 'one' +Implicit conversion from float 1.5 to int loses precision +isset($ao[1.5]): true +Implicit conversion from float 1.5 to int loses precision +seek(1.5): 'one' diff --git a/ext/spl/tests/ArrayObject/gh17935.phpt b/ext/spl/tests/ArrayObject/gh17935.phpt new file mode 100644 index 000000000000..123226208d1c --- /dev/null +++ b/ext/spl/tests/ArrayObject/gh17935.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-17935 (Assertion failure Zend/zend_hash.c when appending to serialized ArrayObject) +--FILE-- +__serialize(); +$o['a'] = 'b'; +var_dump($data[1], $o->getArrayCopy()); + +$o = new ArrayIterator(['a' => 1]); +$data = $o->__serialize(); +unset($o['a']); +var_dump($data[1], $o->getArrayCopy()); +?> +--EXPECT-- +array(0) { +} +array(1) { + ["a"]=> + string(1) "b" +} +array(1) { + ["a"]=> + int(1) +} +array(0) { +}