Fix GH-23385: Use-after-free in SplDoublyLinkedList::serialize() - #23387
Open
Alb3e3 wants to merge 2 commits into
Open
Fix GH-23385: Use-after-free in SplDoublyLinkedList::serialize()#23387Alb3e3 wants to merge 2 commits into
Alb3e3 wants to merge 2 commits into
Conversation
The IS_ARRAY case of php_var_serialize_intern() walked the array's HashTable without holding a reference across php_var_serialize_nested_data(), which recurses into user hooks (__serialize, __sleep, Serializable::serialize). A hook that grows the same array through a by-reference alias reallocs the backing store mid-walk, so the iterator reads freed memory. Hold a ref across the walk, as the object path and var_dump/var_export already do, so the append separates a copy instead of reallocating in place. This is a backport of cc8abaf (phpGH-22714), which landed on master only. phpGH-23385 needs it on this branch as well.
SplDoublyLinkedList::serialize() walks the list and hands each element's zval to php_var_serialize(). Serializing an element can call back into userland (__serialize, __sleep, Serializable::serialize), and that code can remove the very element being serialized from the list. The loop already takes a reference on the *next* element for exactly this reason, but not on the current one. Once offsetUnset() drops the last reference to it the element is freed, while php_var_serialize() still holds a pointer into it as its `struc` argument and dereferences it for every value it walks. Take a reference on the current element as well, so it survives the call. The element is already designed to outlive its removal from the list, see the "Keep consistency if element is kept alive" path in offsetUnset().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes GH-23385.
The SPL side
SplDoublyLinkedList::serialize()walks the list and passes each element's zval straight tophp_var_serialize():Serializing an element can re-enter userland (
__serialize,__sleep,Serializable::serialize), and that code can remove the element that is being serialized. The loop already anticipates this for the next element but not for the current one, sooffsetUnset()drops the last reference and frees it whilephp_var_serialize()still uses¤t->dataas itsstrucargument.The element is already built to outlive its removal from the list, see the "Keep consistency if element is kept alive" branch in
offsetUnset(), so taking a reference for the duration of the call is all that is needed.Why there are two commits
The reproducer in the issue frees two different things, and only one of them is the SPL bug.
offsetUnset()also destroyselement->data, which drops the array's last reference whilephp_var_serialize_nested_data()is iterating it. That half was already fixed on master by cc8abaf (GH-22714), which holds a ref on the HashTable across the walk, but that commit never made it to PHP-8.4/8.5. So on this branch the issue's reproducer still crashes there even with the SPL fix applied.The first commit is that backport, unchanged apart from the surrounding
incomplete_classargument style on this branch, together with its test. Please drop it if you would rather merge cc8abaf up yourself; the second commit stands on its own and merges up cleanly.Verification
Built PHP-8.4 (
--disable-all --enable-debug) with ASan andUSE_ZEND_ALLOC=0.Before, the issue's reproducer:
After, both that reproducer and a variant that hits only the SPL half (a nested array after the object, so the walk dereferences
strucagain once the element is gone) run clean, with no leaks reported.ext/spl/andext/standard/tests/serialize/: 920 pass, same 4 pre-existing failures as an unpatched checkout of this branch in the same ASan build (RecursiveIteratorIterator_dtor_order, bug79710, bug67247, bug77751). Unpatched: 918 pass, so the delta is exactly the two added tests.The new
gh23385.phptcovers three shapes: an element removing itself, an element removing its successor, and an element clearing the whole list.Note on AI use
I used Claude Code while working on this. I reproduced the crash, wrote and reviewed the change, and ran the test suites myself.