[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, &current->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 `&current->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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.