Skip to content
Open
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 @@ -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)

Expand Down
94 changes: 83 additions & 11 deletions ext/spl/spl_array.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,75 @@ 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);
spl_array_skip_protected(intern, ht);
}
/* }}} */

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;
}
Expand Down Expand Up @@ -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);
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -592,14 +658,17 @@ 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) {
zend_illegal_container_offset(object->ce->name, offset, BP_VAR_IS);
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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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 */
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
ArrayObject storage exchanged by an error handler while creating an element
--FILE--
<?php
$ao = new ArrayObject(['a' => 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"
}
82 changes: 82 additions & 0 deletions ext/spl/tests/ArrayObject/ArrayObject_separate_on_write.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
--TEST--
ArrayObject and ArrayIterator separate their storage from the arrays they hand out before writing to it
--FILE--
<?php
// __serialize() and __debugInfo() hand out the storage without copying it:
// every write path separates the storage first

function check(string $label, callable $write, array $classes = ['ArrayIterator', 'ArrayObject']) {
foreach ($classes as $class) {
$expected = ['a' => 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}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
ArrayObject storage shared by an error handler while creating an element
--FILE--
<?php
$ao = new ArrayObject(['a' => 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)
}
}
Loading
Loading