Commit 0d6ab5321d2 for php.net
commit 0d6ab5321d237286aa068f08c817594a421a6e0b
Author: Eyüp Can Akman <eyupcanakman@gmail.com>
Date: Fri Jul 17 03:01:38 2026 +0300
Fix GH-22779: mb_strrpos() wrong result in a non-UTF-8 encoding
mb_find_strpos() converts the haystack and needle to UTF-8 and does the length and offset math on the converted strings.
The negative-offset branch computed the needle length from the raw needle instead of needle_u8.
mb_fast_strlen_utf8() reads those bytes as UTF-8, so the count is wrong whenever they do not decode one to one.
A byte in 0x80-0xBF (ISO-8859-1) undercounts, and a wider encoding such as an ASCII needle in UTF-16 overcounts, so the reverse-search window is shifted.
Use needle_u8, the converted needle the rest of the branch already searches with.
diff --git a/NEWS b/NEWS
index 6b42bbf1cbd..e4667b1d3b9 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,10 @@ PHP NEWS
attribute node that collides by local name with a namespaced
attribute). (David Carlier)
+- MBString:
+ . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative
+ offset in a non-UTF-8 encoding). (Eyüp Can Akman)
+
- Sockets:
. Fixed various memory related issues in ext/sockets. (David Carlier)
diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c
index c3394e79f02..4893390a826 100644
--- a/ext/mbstring/mbstring.c
+++ b/ext/mbstring/mbstring.c
@@ -1931,7 +1931,7 @@ static size_t mb_find_strpos(zend_string *haystack, zend_string *needle, const m
} else if (offset >= 0) {
found_pos = zend_memnrstr((const char*)offset_pointer, ZSTR_VAL(needle_u8), ZSTR_LEN(needle_u8), ZSTR_VAL(haystack_u8) + ZSTR_LEN(haystack_u8));
} else {
- size_t needle_len = pointer_to_offset_utf8((unsigned char*)ZSTR_VAL(needle), (unsigned char*)ZSTR_VAL(needle) + ZSTR_LEN(needle));
+ size_t needle_len = pointer_to_offset_utf8((unsigned char*)ZSTR_VAL(needle_u8), (unsigned char*)ZSTR_VAL(needle_u8) + ZSTR_LEN(needle_u8));
offset_pointer = offset_to_pointer_utf8(offset_pointer, (unsigned char*)ZSTR_VAL(haystack_u8) + ZSTR_LEN(haystack_u8), needle_len);
if (!offset_pointer) {
offset_pointer = (unsigned char*)ZSTR_VAL(haystack_u8) + ZSTR_LEN(haystack_u8);
diff --git a/ext/mbstring/tests/gh22779.phpt b/ext/mbstring/tests/gh22779.phpt
new file mode 100644
index 00000000000..2983f4e1194
--- /dev/null
+++ b/ext/mbstring/tests/gh22779.phpt
@@ -0,0 +1,34 @@
+--TEST--
+GH-22779: mb_strrpos() wrong result for a negative offset in a non-UTF-8 encoding
+--EXTENSIONS--
+mbstring
+--FILE--
+<?php
+/* mb_strrpos() with a negative offset must return the correct position in non-UTF-8 encodings. */
+$haystack = "\xA9\xA9X";
+$needle = "\xA9";
+foreach ([-1, -2, -3] as $offset) {
+ var_dump(mb_strrpos($haystack, $needle, $offset, 'ISO-8859-1'));
+}
+var_dump(mb_strrpos("X\xA9", "\xA9", -1, 'ISO-8859-1'));
+var_dump(mb_strrpos("\x95Z\x95Z", "\x95", -1, 'Windows-1252'));
+var_dump(mb_strrpos("\x95Z\x95Z", "\x95", -2, 'Windows-1252'));
+// Two-byte needle in a single-byte encoding.
+var_dump(mb_strrpos("\xA9\xB0\xA9\xB0Z", "\xA9\xB0", -2, 'ISO-8859-1'));
+// Multibyte: "ああA" in Shift_JIS, needle "あ" (\x82\xA0).
+var_dump(mb_strrpos("\x82\xA0\x82\xA0A", "\x82\xA0", -2, 'SJIS'));
+var_dump(mb_strrpos("\x82\xA0\x82\xA0A", "\x82\xA0", -3, 'SJIS'));
+// UTF-16: an ASCII needle miscounts the other way.
+var_dump(mb_strrpos("\x00A\x00X\x00A", "\x00A", -2, 'UTF-16BE'));
+?>
+--EXPECT--
+int(1)
+int(1)
+int(0)
+int(1)
+int(2)
+int(2)
+int(2)
+int(1)
+int(0)
+int(0)