Skip to content

Commit daf0765

Browse files
committed
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
1 parent 8b1668d commit daf0765

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ PHP NEWS
1111
registrations are freed while still reachable from the cycle collector.
1212
(Ilia Alshanetsky)
1313

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

1519
10 Sep 2026, PHP 8.6.0beta3
1620

‎ext/spl/spl_dllist.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,12 +962,16 @@ PHP_METHOD(SplDoublyLinkedList, serialize)
962962

963963
/* elements */
964964
while (current) {
965+
zval data;
966+
965967
smart_str_appendc(&buf, ':');
966968
next = current->next;
967969

968970
SPL_LLIST_CHECK_ADDREF(next);
969971

970-
php_var_serialize(&buf, &current->data, &var_hash);
972+
ZVAL_COPY(&data, &current->data);
973+
php_var_serialize(&buf, &data, &var_hash);
974+
zval_ptr_dtor(&data);
971975

972976
SPL_LLIST_CHECK_DELREF_EX(next, break;);
973977

‎ext/spl/tests/gh23385.phpt‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
--TEST--
2+
GH-23385 (Use-after-free in SplDoublyLinkedList::serialize())
3+
--CREDITS--
4+
f9j2n6nd8k-eng
5+
--FILE--
6+
<?php
7+
8+
class RemoveSelf {
9+
public function __serialize(): array {
10+
global $list;
11+
unset($list[0]);
12+
return [];
13+
}
14+
}
15+
16+
$list = new SplDoublyLinkedList();
17+
$list->push([new RemoveSelf(), [1, 2, 3]]);
18+
$list->push("tail");
19+
var_dump($list->serialize());
20+
var_dump($list->count());
21+
22+
class RemoveNext {
23+
public function __serialize(): array {
24+
global $list2;
25+
unset($list2[1]);
26+
return [];
27+
}
28+
}
29+
30+
$list2 = new SplDoublyLinkedList();
31+
$list2->push(new RemoveNext());
32+
$list2->push("removed");
33+
$list2->push("after");
34+
var_dump($list2->serialize());
35+
var_dump($list2->count());
36+
37+
class RemoveAll {
38+
public function __serialize(): array {
39+
global $list3;
40+
while (!$list3->isEmpty()) {
41+
$list3->pop();
42+
}
43+
return [];
44+
}
45+
}
46+
47+
$list3 = new SplDoublyLinkedList();
48+
$list3->push([new RemoveAll(), [1, 2]]);
49+
$list3->push("x");
50+
$list3->push("y");
51+
var_dump($list3->serialize());
52+
var_dump($list3->count());
53+
54+
class RemoveHolder {
55+
public function __serialize(): array {
56+
global $list4;
57+
unset($list4[0]);
58+
return [];
59+
}
60+
}
61+
62+
class Holder {
63+
public $first;
64+
public $second = "second";
65+
public $third = "third";
66+
}
67+
68+
$holder = new Holder();
69+
$holder->first = new RemoveHolder();
70+
71+
$list4 = new SplDoublyLinkedList();
72+
$list4->push($holder);
73+
unset($holder);
74+
$list4->push("tail");
75+
var_dump($list4->serialize());
76+
var_dump($list4->count());
77+
78+
?>
79+
--EXPECT--
80+
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";"
81+
int(1)
82+
string(27) "i:0;:O:10:"RemoveNext":0:{}"
83+
int(2)
84+
string(61) "i:0;:a:2:{i:0;O:9:"RemoveAll":0:{}i:1;a:2:{i:0;i:1;i:1;i:2;}}"
85+
int(0)
86+
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";"
87+
int(1)

0 commit comments

Comments
 (0)