Commit b747e19eb5e for php.net

commit b747e19eb5e77071783c626e5d0bceee7f230bc6
Author: Weilin Du <108666168+LamentXU123@users.noreply.github.com>
Date:   Thu Jun 4 18:15:47 2026 +0800

    ext/intl: Fix UConverter::transcode() substitution length truncation (#22199)

    Now, in UConverter::transcode() we masked the from_subst and to_subst option string length with 0x7F before passing them to ucnv_setSubstChars(). This makes every string with length greater than 127 be silently truncated.

    This PR fix this bug, see #22199

diff --git a/NEWS b/NEWS
index 3c4d6993f46..5fc78a65515 100644
--- a/NEWS
+++ b/NEWS
@@ -73,6 +73,8 @@ PHP                                                                        NEWS
     types. (Weilin Du)
   . Fixed MessageFormatter::parse() and parseMessage() returning PHP_INT_MIN
     as float rather than int on 64-bit platforms. (Weilin Du)
+  . Fixed UConverter::transcode() silently truncating from_subst and to_subst
+    option lengths greater than 127 bytes. (Weilin Du)

 - JSON:
   . Enriched JSON last error / exception message with error location.
diff --git a/UPGRADING b/UPGRADING
index 95299dd5117..543e1996e1b 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -44,6 +44,9 @@ PHP 8.6 UPGRADE NOTES
     int, rather than float, on 64-bit platforms when parsing integer values.
   . The $type parameter of IntlBreakIterator::getPartsIterator() has been
     changed from string to int to match the underlying implementation.
+  . UConverter::transcode() now rejects from_subst and to_subst option values
+    longer than 127 bytes instead of silently truncating the length before
+    passing it to ICU.

 - PCNTL:
   . pcntl_alarm() now raises a ValueError if the seconds argument is
diff --git a/ext/intl/converter/converter.cpp b/ext/intl/converter/converter.cpp
index 597746b8fac..88f05136bbb 100644
--- a/ext/intl/converter/converter.cpp
+++ b/ext/intl/converter/converter.cpp
@@ -682,6 +682,16 @@ static zend_string* php_converter_do_convert(UConverter *dest_cnv,
 }
 /* }}} */

+static void php_converter_set_subst_chars(UConverter *cnv, zend_string *subst, UErrorCode *error)
+{
+	if (ZSTR_LEN(subst) > SCHAR_MAX) {
+		*error = U_ILLEGAL_ARGUMENT_ERROR;
+		return;
+	}
+
+	ucnv_setSubstChars(cnv, ZSTR_VAL(subst), (int8_t) ZSTR_LEN(subst), error);
+}
+
 /* {{{ */
 #define UCNV_REASON_CASE(v) case (UCNV_ ## v) : RETURN_STRINGL( "REASON_" #v , sizeof( "REASON_" #v ) - 1);
 PHP_METHOD(UConverter, reasonText) {
@@ -761,13 +771,13 @@ PHP_METHOD(UConverter, transcode) {
 				(tmpzval = zend_hash_str_find_deref(Z_ARRVAL_P(options), "from_subst", sizeof("from_subst") - 1)) != NULL &&
 				Z_TYPE_P(tmpzval) == IS_STRING) {
 				error = U_ZERO_ERROR;
-				ucnv_setSubstChars(src_cnv, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval) & 0x7F, &error);
+				php_converter_set_subst_chars(src_cnv, Z_STR_P(tmpzval), &error);
 			}
 			if (U_SUCCESS(error) &&
 				(tmpzval = zend_hash_str_find_deref(Z_ARRVAL_P(options), "to_subst", sizeof("to_subst") - 1)) != NULL &&
 				Z_TYPE_P(tmpzval) == IS_STRING) {
 				error = U_ZERO_ERROR;
-				ucnv_setSubstChars(dest_cnv, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval) & 0x7F, &error);
+				php_converter_set_subst_chars(dest_cnv, Z_STR_P(tmpzval), &error);
 			}
 		}

diff --git a/ext/intl/tests/uconverter_transcode_subst_length.phpt b/ext/intl/tests/uconverter_transcode_subst_length.phpt
new file mode 100644
index 00000000000..93001b16680
--- /dev/null
+++ b/ext/intl/tests/uconverter_transcode_subst_length.phpt
@@ -0,0 +1,20 @@
+--TEST--
+UConverter::transcode() rejects too long substitution strings
+--EXTENSIONS--
+intl
+--INI--
+intl.use_exceptions=false
+--FILE--
+<?php
+$subst = str_repeat('A', 129);
+
+var_dump(UConverter::transcode('abc', 'UTF-8', 'ASCII', ['from_subst' => $subst]));
+echo intl_get_error_message(), "\n";
+var_dump(UConverter::transcode('abc', 'UTF-8', 'ASCII', ['to_subst' => $subst]));
+echo intl_get_error_message(), "\n";
+?>
+--EXPECT--
+bool(false)
+UConverter::transcode(): returned error 1: U_ILLEGAL_ARGUMENT_ERROR: U_ILLEGAL_ARGUMENT_ERROR
+bool(false)
+UConverter::transcode(): returned error 1: U_ILLEGAL_ARGUMENT_ERROR: U_ILLEGAL_ARGUMENT_ERROR