[GIT-PULLS] [php-src] PR #23387: Fix GH-23385: Use-after-free in SplDoublyLinkedList::serialize()
[email protected] (Alb3e3)
| Newsgroups | php.git-pulls |
|---|---|
| Message-ID | <VYgfv2lpb4EtXr3lbMuTN5nkeF0dmgbDdqN5W6BymWI@main.internal.php.net> |
Pull Request: https://github.com/php/php-src/pull/23387
Author: Alb3e3
Fixes GH-23385.
### The SPL side
`SplDoublyLinkedList::serialize()` walks the list and passes each element's zval straight to `php_var_serialize()`:
```c
while (current) {
next = current->next;
SPL_LLIST_CHECK_ADDREF(next);
php_var_serialize(&buf, ¤t->data, &var_hash);
SPL_LLIST_CHECK_DELREF_EX(next, break;);
current = next;
}
```
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, so `offsetUnset()` drops the last reference and frees it while `php_var_serialize()` still uses `¤t->data` as its `struc` argument.
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 destroys `element->data`, which drops the array's last reference while `php_var_serialize_nested_data()` is iterating it. That half was already fixed on master by cc8abaf9f (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_class` argument style on this branch, together with its test. Please drop it if you would rather merge cc8abaf9f up yourself; the second commit stands on its own and merges up cleanly.
### Verification
Built PHP-8.4 (`--disable-all --enable-debug`) with ASan and `USE_ZEND_ALLOC=0`.
Before, the issue's reproducer:
```
==1803822==ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 ... php_var_serialize_nested_data var.c:1009
freed by ... zim_SplDoublyLinkedList_offsetUnset spl_dllist.c:758
```
After, both that reproducer and a variant that hits only the SPL half (a nested array after the object, so the walk dereferences `struc` again once the element is gone) run clean, with no leaks reported.
`ext/spl/` and `ext/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.phpt` covers 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.