Commit 9f23a4de630 for php.net

commit 9f23a4de630864749da25da803080e090de93ab1
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Tue Aug 18 22:32:57 2026 +0800

    Bound getSingletonPos before reading the next separator (#23350)

    getSingletonPos() read str[i+2] after the last '-' or '_' in a locale,
    one byte past the NUL for strings such as "en-". Require i+2 to be inside
    the string before testing it for another separator.

    Output otherwise matches the unfixed parser unless that heap byte happens to
    be another separator, which is why GH-22498 was previously closed.

    Also audit getStrrtokenPos() and both callers, get_icu_value_internal() and
    get_private_subtags(); their bounds handling is already safe.

    Co-authored-by: Xuyang Zhang <119476662+kn1g78@users.noreply.github.com>
    Closes #23350

diff --git a/NEWS b/NEWS
index 5ab1602dbd6..4cafbbd5a90 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,8 @@ PHP                                                                        NEWS
     the ICU constructor adopts the TimeZone. (iliaal)
   . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions
     for UTF-8 strings). (ColumbusLabs)
+  . Fixed Locale::parseLocale() reading past a trailing '-' or '_'.
+    (iliaal, Xuyang Zhang)

 - Opcache:
   . Fixed opcache.protect_memory race under ZTS. (realFlowControl)
diff --git a/ext/intl/locale/locale_methods.c b/ext/intl/locale/locale_methods.c
index b5d48257338..e3894b6f28f 100644
--- a/ext/intl/locale/locale_methods.c
+++ b/ext/intl/locale/locale_methods.c
@@ -279,7 +279,7 @@ static zend_off_t getSingletonPos(const char* str)
 					break;
 				} else {
 					/* delimiter found; check for singleton */
-					if( isIDSeparator(*(str+i+2)) ){
+					if( (size_t)i + 2 < len && isIDSeparator(*(str+i+2)) ){
 						/* a singleton; so send the position of separator before singleton */
 						result = i+1;
 						break;
diff --git a/ext/intl/tests/locale_parse_trailing_separator.phpt b/ext/intl/tests/locale_parse_trailing_separator.phpt
new file mode 100644
index 00000000000..96b2139b72c
--- /dev/null
+++ b/ext/intl/tests/locale_parse_trailing_separator.phpt
@@ -0,0 +1,45 @@
+--TEST--
+Locale::parseLocale() does not read past a trailing '-' or '_'
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+/* Enough lengths that the byte past the end clears the allocation. */
+foreach (['-', '_'] as $sep) {
+    for ($len = 1; $len <= 64; $len++) {
+        Locale::parseLocale(str_repeat('a', $len) . $sep);
+    }
+}
+
+$locales = [
+    'en-',
+    'foo-',
+    'en_US-',
+    'en_',
+    'de-CH-x-',
+];
+
+foreach ($locales as $locale) {
+    echo $locale, ': ';
+    var_export(Locale::parseLocale($locale));
+    echo "\n";
+}
+?>
+--EXPECT--
+en-: array (
+  'language' => 'en',
+)
+foo-: array (
+  'language' => 'foo',
+)
+en_US-: array (
+  'language' => 'en',
+  'region' => 'US',
+)
+en_: array (
+  'language' => 'en',
+)
+de-CH-x-: array (
+  'language' => 'de',
+  'region' => 'CH',
+)