Skip to content

Fix GH-17935: Separate ArrayObject and ArrayIterator storage on write - #25432

Open
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:spl-array-separate-on-write
Open

nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:spl-array-separate-on-write

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Split from #23945.

ArrayObject and ArrayIterator assume that they're the only owner of their storage. But __serialize(), __debugInfo(), var_export(), json_encode() and the sort methods return or borrow this storage without a copy. A write to the object then also changes the array that was returned, and debug builds fail the refcount assertion of zend_hash.c. That's GH-17935.

This PR separates the storage before any write. When that happens during an iteration, the iterator moves to the copy of its position that zend_array_dup() registers on the new table. Object-backed storage isn't changed.

An error handler can now separate the storage by writing to the object. So read paths fetch the table after the code that can call a handler. This also fixes use-after-frees when a handler calls exchangeArray() during a read with a null offset, or while an element is created.

This is the delayed separation discussed in #17942. It overlaps with #20725, which separates at three write sites, and it fixes the array-backed cases of #22921.

foreach over an ArrayObject needs about 3% more instructions (callgrind, 101 elements).

/cc @ndossche @iluuu1994 @alexandre-daubois @iliaal

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant