Commit 4efca409534 for php.net

commit 4efca40953435efce4511e494d28c8496b51b712
Author: Weilin Du <108666168+LamentXU123@users.noreply.github.com>
Date:   Sat Jun 20 13:07:19 2026 +0800

    ext/intl: Fix Locale::lookup() fallback on invalid language tags (#22306)

    Locale::lookup() and locale_lookup() should not return the fallback locale
    when canonicalizing a language tag fails. Returning the fallback hid the
    intl error raised by lookup_loc_range() for invalid language tags.

    Return NULL in that error case instead, while preserving the exception path
    when intl.use_exceptions is enabled.

    Closes #22306

diff --git a/NEWS b/NEWS
index 32bb0b49f34..df93cb5b77b 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,8 @@ PHP                                                                        NEWS
     for invalid display types. (Weilin Du)
   . Fixed Spoofchecker restriction-level APIs to only be exposed with ICU 53
     and later. (Graham Campbell)
+  . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the
+    fallback locale when a language tag cannot be canonicalized. (Weilin Du)

 - mysqli:
   . Fix stmt->query leak in mysqli_execute_query() validation errors.
diff --git a/ext/intl/locale/locale_methods.c b/ext/intl/locale/locale_methods.c
index e7cc9d8364c..b5d48257338 100644
--- a/ext/intl/locale/locale_methods.c
+++ b/ext/intl/locale/locale_methods.c
@@ -1435,14 +1435,15 @@ static zend_string* lookup_loc_range(const char* loc_range, HashTable* hash_arr,
 			zend_argument_type_error(2, "must only contain string values");
 			LOOKUP_CLEAN_RETURN(NULL);
 		}
-		cur_arr[cur_arr_len*2] = estrndup(Z_STRVAL_P(ele_value), Z_STRLEN_P(ele_value));
-		result = strToMatch(Z_STRVAL_P(ele_value), cur_arr[cur_arr_len*2]);
+		i = cur_arr_len*2;
+		cur_arr[i] = estrndup(Z_STRVAL_P(ele_value), Z_STRLEN_P(ele_value));
+		cur_arr_len++;
+		result = strToMatch(Z_STRVAL_P(ele_value), cur_arr[i]);
 		if(result == 0) {
 			intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "lookup_loc_range: unable to canonicalize lang_tag", 0);
 			LOOKUP_CLEAN_RETURN(NULL);
 		}
-		cur_arr[cur_arr_len*2+1] = Z_STRVAL_P(ele_value);
-		cur_arr_len++ ;
+		cur_arr[i+1] = Z_STRVAL_P(ele_value);
 	} ZEND_HASH_FOREACH_END(); /* end of for */

 	/* Canonicalize array elements */
@@ -1562,6 +1563,15 @@ PHP_FUNCTION(locale_lookup)
 	}

 	result_str = lookup_loc_range(loc_range, hash_arr, boolCanonical);
+	if (EG(exception)) {
+		RETURN_THROWS();
+	}
+	if (U_FAILURE(intl_error_get_code(NULL))) {
+		if (result_str) {
+			zend_string_release_ex(result_str, 0);
+		}
+		RETURN_NULL();
+	}
 	if(result_str == NULL || ZSTR_VAL(result_str)[0] == '\0') {
 		if( fallback_loc_str ) {
 			result_str = zend_string_copy(fallback_loc_str);
diff --git a/ext/intl/tests/locale_lookup_invalid_language_tag.phpt b/ext/intl/tests/locale_lookup_invalid_language_tag.phpt
new file mode 100644
index 00000000000..6f7c7517f95
--- /dev/null
+++ b/ext/intl/tests/locale_lookup_invalid_language_tag.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Locale::lookup() returns null for invalid language tags
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+var_dump(Locale::lookup([''], 'de-DE', false, 'en-US'));
+var_dump(intl_get_error_message());
+
+var_dump(locale_lookup([''], 'de-DE', false, 'en-US'));
+var_dump(intl_get_error_message());
+
+ini_set('intl.use_exceptions', '1');
+
+try {
+    Locale::lookup([''], 'de-DE', false, 'en-US');
+} catch (IntlException $e) {
+    echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+    locale_lookup([''], 'de-DE', false, 'en-US');
+} catch (IntlException $e) {
+    echo $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+NULL
+string(75) "lookup_loc_range: unable to canonicalize lang_tag: U_ILLEGAL_ARGUMENT_ERROR"
+NULL
+string(75) "lookup_loc_range: unable to canonicalize lang_tag: U_ILLEGAL_ARGUMENT_ERROR"
+lookup_loc_range: unable to canonicalize lang_tag
+lookup_loc_range: unable to canonicalize lang_tag