Commit daf076548b8 for php.net

commit daf076548b8175e6404eb654a03f6a4852663395
Author: David Carlier <devnexen@gmail.com>
Date:   Wed Aug 19 19:18:25 2026 +0100

    ext/spl: SplDoublyLinkedList::serialize() use-after-free on element removal.

    Fix GH-23385

    The serialization loop passed php_var_serialize() a pointer into the list
    element itself, so a userland __serialize() unsetting that entry freed both
    the element and its payload while the serializer was still walking them.
    Serialize a copy of the element data instead, which outlives the callback.

    Close GH-23388

diff --git a/NEWS b/NEWS
index 4efc8258fc2..1212b4fae18 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,10 @@ PHP                                                                        NEWS
     registrations are freed while still reachable from the cycle collector.
     (Ilia Alshanetsky)

+- SPL:
+  . Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when
+    __serialize() removes an element). (David Carlier)
+

 10 Sep 2026, PHP 8.6.0beta3

diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c
index 2ef5d7a07db..07b1bdb398b 100644
--- a/ext/spl/spl_dllist.c
+++ b/ext/spl/spl_dllist.c
@@ -962,12 +962,16 @@ PHP_METHOD(SplDoublyLinkedList, serialize)

 	/* elements */
 	while (current) {
+		zval data;
+
 		smart_str_appendc(&buf, ':');
 		next = current->next;

 		SPL_LLIST_CHECK_ADDREF(next);

-		php_var_serialize(&buf, &current->data, &var_hash);
+		ZVAL_COPY(&data, &current->data);
+		php_var_serialize(&buf, &data, &var_hash);
+		zval_ptr_dtor(&data);

 		SPL_LLIST_CHECK_DELREF_EX(next, break;);

diff --git a/ext/spl/tests/gh23385.phpt b/ext/spl/tests/gh23385.phpt
new file mode 100644
index 00000000000..f31aa02a17c
--- /dev/null
+++ b/ext/spl/tests/gh23385.phpt
@@ -0,0 +1,87 @@
+--TEST--
+GH-23385 (Use-after-free in SplDoublyLinkedList::serialize())
+--CREDITS--
+f9j2n6nd8k-eng
+--FILE--
+<?php
+
+class RemoveSelf {
+    public function __serialize(): array {
+        global $list;
+        unset($list[0]);
+        return [];
+    }
+}
+
+$list = new SplDoublyLinkedList();
+$list->push([new RemoveSelf(), [1, 2, 3]]);
+$list->push("tail");
+var_dump($list->serialize());
+var_dump($list->count());
+
+class RemoveNext {
+    public function __serialize(): array {
+        global $list2;
+        unset($list2[1]);
+        return [];
+    }
+}
+
+$list2 = new SplDoublyLinkedList();
+$list2->push(new RemoveNext());
+$list2->push("removed");
+$list2->push("after");
+var_dump($list2->serialize());
+var_dump($list2->count());
+
+class RemoveAll {
+    public function __serialize(): array {
+        global $list3;
+        while (!$list3->isEmpty()) {
+            $list3->pop();
+        }
+        return [];
+    }
+}
+
+$list3 = new SplDoublyLinkedList();
+$list3->push([new RemoveAll(), [1, 2]]);
+$list3->push("x");
+$list3->push("y");
+var_dump($list3->serialize());
+var_dump($list3->count());
+
+class RemoveHolder {
+    public function __serialize(): array {
+        global $list4;
+        unset($list4[0]);
+        return [];
+    }
+}
+
+class Holder {
+    public $first;
+    public $second = "second";
+    public $third = "third";
+}
+
+$holder = new Holder();
+$holder->first = new RemoveHolder();
+
+$list4 = new SplDoublyLinkedList();
+$list4->push($holder);
+unset($holder);
+$list4->push("tail");
+var_dump($list4->serialize());
+var_dump($list4->count());
+
+?>
+--EXPECT--
+string(83) "i:0;:a:2:{i:0;O:10:"RemoveSelf":0:{}i:1;a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}}:s:4:"tail";"
+int(1)
+string(27) "i:0;:O:10:"RemoveNext":0:{}"
+int(2)
+string(61) "i:0;:a:2:{i:0;O:9:"RemoveAll":0:{}i:1;a:2:{i:0;i:1;i:1;i:2;}}"
+int(0)
+string(120) "i:0;:O:6:"Holder":3:{s:5:"first";O:12:"RemoveHolder":0:{}s:6:"second";s:6:"second";s:5:"third";s:5:"third";}:s:4:"tail";"
+int(1)