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, ¤t->data, &var_hash);
+ ZVAL_COPY(&data, ¤t->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)