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',
+)