diff --git a/NEWS b/NEWS index 4efc8258fc2d..5182713edfc5 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,14 @@ 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) + +- 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/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/mbstring/mbstring.c b/ext/mbstring/mbstring.c index 4059d8d35fd1..ed3e8bd1cf44 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -1902,6 +1902,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) 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) 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;