Commit 6aff7e384f3 for php

commit 6aff7e384f3c605457f25b74993a9d2d7d6174c1
Author: Weilin Du <weilindu@php.net>
Date:   Thu Oct 8 00:00:42 2026 +0800

    ext/intl: Fix grapheme_extract() next offset (#24160)

    When the starting position lies inside a multibyte UTF-8 character,
    grapheme_extract() advances the cursor but reports a next offset based on
    the original position. Derive the adjusted offset from the cursor for both
    extraction paths.

    Add regression coverage for extraction modes, ASCII and Unicode tails,
    and negative starting offsets, and document the fix in NEWS.

    Co-authored-by: David Carlier <devnexen@gmail.com>

diff --git a/NEWS b/NEWS
index 999d1f6fc5a..64c94bbbebd 100644
--- a/NEWS
+++ b/NEWS
@@ -108,6 +108,8 @@ PHP                                                                        NEWS
     haystacks. (Weilin Du)
   . Fixed grapheme_strrpos() and grapheme_strripos() skipping overlapping matches
     when using a negative offset. (Weilin Du)
+  . Fixed grapheme_extract() returning an incorrect next offset when starting
+    inside a multibyte UTF-8 character. (Weilin Du, David Carlier)

 - Lexbor:
   . Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a
diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c
index 6a9233b6b6a..50e40cffc01 100644
--- a/ext/intl/grapheme/grapheme_string.c
+++ b/ext/intl/grapheme/grapheme_string.c
@@ -786,7 +786,8 @@ PHP_FUNCTION(grapheme_extract)
 		}
 	}

-	str_len -= (pstr - str);
+	lstart = pstr - str;
+	str_len -= lstart;

 	/* if the string is all ASCII up to size+1 - or str_len whichever is first - then we are done.
 		(size + 1 because the size-th character might be the beginning of a grapheme cluster)
@@ -795,7 +796,7 @@ PHP_FUNCTION(grapheme_extract)
 	if ( -1 != grapheme_ascii_check((unsigned char *)pstr, MIN(size + 1, str_len)) ) {
 		size_t nsize = MIN(size, str_len);
 		if ( NULL != next ) {
-			ZEND_TRY_ASSIGN_REF_LONG(next, start + nsize);
+			ZEND_TRY_ASSIGN_REF_LONG(next, lstart + nsize);
 		}
 		RETURN_STRINGL(pstr, nsize);
 	}
@@ -829,7 +830,7 @@ PHP_FUNCTION(grapheme_extract)
 	ubrk_close(bi);

 	if ( NULL != next ) {
-		ZEND_TRY_ASSIGN_REF_LONG(next, start + ret_pos);
+		ZEND_TRY_ASSIGN_REF_LONG(next, lstart + ret_pos);
 	}

 	RETURN_STRINGL(((char *)pstr), ret_pos);
diff --git a/ext/intl/tests/grapheme_extract_next_offset.phpt b/ext/intl/tests/grapheme_extract_next_offset.phpt
new file mode 100644
index 00000000000..07221d5b895
--- /dev/null
+++ b/ext/intl/tests/grapheme_extract_next_offset.phpt
@@ -0,0 +1,39 @@
+--TEST--
+grapheme_extract() includes skipped UTF-8 continuation bytes in the next offset
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$cases = [
+    ["\u{00E9}x", 1, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{1F600}x", 1, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{1F600}x", 2, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{1F600}x", 3, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{00E9}\u{1F600}x", 1, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{00E9}\u{1F600}x", 1, 4, GRAPHEME_EXTR_MAXBYTES],
+    ["\u{00E9}\u{1F600}x", 1, 1, GRAPHEME_EXTR_MAXCHARS],
+    ["\u{1F600}\u{00E9}x", -4, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{00E9}x", -2, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{1F600}x", 0, 1, GRAPHEME_EXTR_COUNT],
+    ["\u{00E9}x", 1, 0, GRAPHEME_EXTR_COUNT],
+];
+
+foreach ($cases as [$string, $start, $size, $type]) {
+    $result = grapheme_extract($string, $size, $type, $start, $next);
+    echo bin2hex($result), ' ', $next, "\n";
+}
+
+?>
+--EXPECT--
+78 3
+78 5
+78 5
+78 5
+f09f9880 6
+f09f9880 6
+f09f9880 6
+c3a9 6
+78 3
+f09f9880 4
+ 1