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) { +}