From daf076548b8175e6404eb654a03f6a4852663395 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 19 Aug 2026 19:18:25 +0100 Subject: [PATCH 1/3] 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 --- NEWS | 4 ++ ext/spl/spl_dllist.c | 6 ++- ext/spl/tests/gh23385.phpt | 87 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 ext/spl/tests/gh23385.phpt diff --git a/NEWS b/NEWS index 4efc8258fc2d..1212b4fae181 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 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) From 74e3a69df1a5e86b1874ec562831ed4db58f7a38 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Sat, 12 Sep 2026 13:28:03 +0800 Subject: [PATCH 2/3] ext/standard: Optimize str_pad() using doubling copies (#23661) This PR optimizes the user-visible `str_pad()` by reducing repeated small copy operations during padding. In `php_str_pad_fill()`, the algorithm now uses doubling copies to grow the written region exponentially. This lowers memcpy counts and improves throughput for large repeated-pattern padding workloads. Added `str_pad_repeated_pattern.phpt` to validate repeated pattern and partial-tail boundaries, including multi-byte patterns and all pad directions. Behavior is unchanged; this is a performance-only change and is now recorded in the PHP 8.6 performance changelog. --- UPGRADING | 1 + ext/standard/string.c | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/UPGRADING b/UPGRADING index a866bc223e42..3980faa4294d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1105,6 +1105,7 @@ PHP 8.6 UPGRADE NOTES . Improved performance of array_walk(). . Improved performance of intval('+0b...', 2) and intval('0b...', 2). . Improved performance of str_split(). + . Improved performance of str_pad(). - URI: . Improved performance of Uri\WhatWg\Url::parse() when collecting diff --git a/ext/standard/string.c b/ext/standard/string.c index 04c3c21b2866..577d4267ed2f 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -5891,13 +5891,15 @@ static void php_str_pad_fill(zend_string *result, size_t pad_chars, const char * return; } + const char *start = p; const char *end = p + pad_chars; - while (p + pad_str_len <= end) { - p = zend_mempcpy(p, pad_str, pad_str_len); - } + size_t len = MIN(pad_str_len, pad_chars); + p = zend_mempcpy(p, pad_str, len); - if (p < end) { - memcpy(p, pad_str, end - p); + /* Double the filled area on each iteration. */ + while (p < end) { + len = MIN(p - start, end - p); + p = zend_mempcpy(p, start, len); } ZSTR_LEN(result) += pad_chars; From cc22056f9db571859b2662b2f58cb69d72638773 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 7 Aug 2026 23:01:47 +0500 Subject: [PATCH 3/3] Fix GH-23106: mb_strpos() overreads truncated UTF-8 haystacks (#23107) offset_to_pointer_utf8() walks the haystack using the UTF-8 mblen table. For a truncated multibyte sequence, the lead byte's table entry can exceed the remaining bytes and leave the search start pointer past the end. Passing that pointer to zend_memnstr() triggers an assertion in debug builds and an out-of-bounds read in release builds, producing bogus offsets or a crash for sufficiently long haystacks. Clamp the pointer to the end of the string, as mb_str_split() already does for the same table walk. Add regression coverage for forward and reverse searches, negative offsets, and offsets beyond the haystack. Fixes GH-23106. Closes #23107 --- NEWS | 4 ++++ ext/mbstring/mbstring.c | 3 +++ ext/mbstring/tests/gh23106.phpt | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 ext/mbstring/tests/gh23106.phpt diff --git a/NEWS b/NEWS index 33e9859d219e..c367688672c9 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,10 @@ PHP NEWS registrations are freed while still reachable from the cycle collector. (Ilia Alshanetsky) +- MBString: + . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in + a truncated UTF-8 sequence). (Lazizbek Ergashev) + - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c index 4893390a8264..6fbeb43fc1b4 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -1891,6 +1891,9 @@ static unsigned char* offset_to_pointer_utf8(unsigned char *str, unsigned char * } pos += u8_tbl[*pos]; } + if (pos > end) { + pos = end; + } return pos; } } diff --git a/ext/mbstring/tests/gh23106.phpt b/ext/mbstring/tests/gh23106.phpt new file mode 100644 index 000000000000..f55df0f2f081 --- /dev/null +++ b/ext/mbstring/tests/gh23106.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence) +--EXTENSIONS-- +mbstring +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +bool(false) +bool(false) +bool(false) +int(1) +mb_strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)