From 3994d016eacc440d6146d5ca25e18c9eb879a263 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 19 Aug 2026 19:18:25 +0100 Subject: [PATCH] ext/spl: SplDoublyLinkedList::serialize() use-after-free on element removal. Fix #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. --- ext/spl/spl_dllist.c | 6 ++- ext/spl/tests/gh23385.phpt | 87 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 ext/spl/tests/gh23385.phpt diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c index 2ef5d7a07dbb..07b1bdb398bf 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 000000000000..f31aa02a17cb --- /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-- +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)