Commit 718c4fda184 for php.net

commit 718c4fda184508d4e39635e3805c5ff8b152cce6
Author: Weilin Du <weilindu@php.net>
Date:   Thu Jul 30 00:12:42 2026 +0800

    ext/intl: Fix Collator sort conversion error handling (#22893)

    Report UTF-8/UTF-16 conversion failures from Collator sort comparisons through
    the intl error mechanism.

    Previously, these paths emitted a warning and continued with an empty string.

    Keep the input array unchanged when sorting cannot complete, preserve the
    active collator state across nested sorts, and stop invoking further
    comparisons after a conversion failure.

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

diff --git a/NEWS b/NEWS
index 0c2fd04ad46..c144b3c62c2 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,12 @@ PHP                                                                        NEWS
     (Weilin Du)
   . Added gmp_powm_sec(). (Weilin Du)

+- Intl:
+  . Fixed Collator::sort(), collator_sort(), Collator::asort(), and
+    collator_asort() to report UTF-8/UTF-16 conversion errors through the intl
+    error handler instead of emitting a warning and continuing with an empty
+    string. (Weilin Du)
+
 30 Jul 2026, PHP 8.6.0alpha3

 - Core:
diff --git a/UPGRADING b/UPGRADING
index d8c8a953766..47a9c023bf3 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -76,6 +76,11 @@ PHP 8.6 UPGRADE NOTES
     IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError
     when the offset argument is not of type int instead of silently converting
     the value.
+  . Collator::sort(), collator_sort(), Collator::asort(), and
+    collator_asort() now report UTF-8/UTF-16 conversion failures during
+    comparison through the intl error mechanism and return false. With
+    intl.use_exceptions enabled, these failures throw IntlException. Previously,
+    these paths emitted a warning and compared the value as an empty string.

 - PCNTL:
   . pcntl_alarm() now raises a ValueError if the seconds argument is
diff --git a/ext/intl/collator/collator_convert.cpp b/ext/intl/collator/collator_convert.cpp
index dd3360a6909..0f6e62f1b55 100644
--- a/ext/intl/collator/collator_convert.cpp
+++ b/ext/intl/collator/collator_convert.cpp
@@ -38,6 +38,20 @@ extern "C" {
 			return retval;                       \
 	}

+static void collator_set_conversion_error(UErrorCode status, const char *message)
+{
+	if (U_SUCCESS(status)) {
+		status = U_MEMORY_ALLOCATION_ERROR;
+	}
+
+	ZEND_ASSERT(INTL_G(current_collator_error) != nullptr);
+	intl_error *err = INTL_G(current_collator_error);
+	intl_error_reset(err);
+	err->code = status;
+	err->custom_error_message = zend_string_init(
+		message, strlen(message), false);
+}
+
 /* {{{ collator_convert_hash_item_from_utf8_to_utf16 */
 static void collator_convert_hash_item_from_utf8_to_utf16(
 	HashTable* hash, zval *hashData, zend_string *hashKey, zend_ulong hashIndex,
@@ -166,11 +180,12 @@ U_CFUNC zval* collator_convert_zstr_utf16_to_utf8( zval* utf16_zval, zval *rv )
 	u8str = intl_convert_utf16_to_utf8(
 		(UChar*) Z_STRVAL_P(utf16_zval), UCHARS( Z_STRLEN_P(utf16_zval) ), &status );
 	if( !u8str ) {
-		php_error( E_WARNING, "Error converting utf16 to utf8 in collator_convert_zval_utf16_to_utf8()" );
-		ZVAL_EMPTY_STRING( rv );
-	} else {
-		ZVAL_NEW_STR( rv, u8str );
+		collator_set_conversion_error(status, "Error converting string from UTF-16 to UTF-8");
+		ZVAL_NULL( rv );
+		return nullptr;
 	}
+
+	ZVAL_NEW_STR( rv, u8str );
 	return rv;
 }
 /* }}} */
@@ -183,11 +198,9 @@ U_CFUNC zend_string *collator_convert_zstr_utf8_to_utf16(zend_string *utf8_str)
 	zend_string *zstr = intl_convert_utf8_to_utf16_zstr(
 			ZSTR_VAL(utf8_str), ZSTR_LEN(utf8_str),
 			&status);
-	// FIXME Or throw error or use intl internal error handler
 	if (U_FAILURE(status)) {
-		php_error(E_WARNING,
-			"Error casting object to string in collator_convert_zstr_utf8_to_utf16()");
-		zstr = ZSTR_EMPTY_ALLOC();
+		collator_set_conversion_error(status, "Error converting string from UTF-8 to UTF-16");
+		return nullptr;
 	}

 	return zstr;
@@ -214,12 +227,19 @@ U_CFUNC zval* collator_convert_object_to_string( zval* obj, zval *rv )
 	{
 		/* cast_object failed => bail out. */
 		zval_ptr_dtor( zstr );
+		ZVAL_NULL( zstr );
+		if( EG(exception) ) {
+			return nullptr;
+		}
 		COLLATOR_CONVERT_RETURN_FAILED( obj );
 	}

 	/* Object wasn't successfully converted => bail out. */
 	if( zstr == nullptr )
 	{
+		if( EG(exception) ) {
+			return nullptr;
+		}
 		COLLATOR_CONVERT_RETURN_FAILED( obj );
 	}

@@ -227,10 +247,11 @@ U_CFUNC zval* collator_convert_object_to_string( zval* obj, zval *rv )
 	zend_string *converted_str = intl_convert_utf8_to_utf16_zstr(
 			Z_STRVAL_P( zstr ), Z_STRLEN_P( zstr ),
 			&status );
-	// FIXME Or throw error or use intl internal error handler
 	if( U_FAILURE( status ) ) {
-		php_error( E_WARNING, "Error casting object to string in collator_convert_object_to_string()" );
-		converted_str = ZSTR_EMPTY_ALLOC();
+		collator_set_conversion_error(status, "Error converting object string from UTF-8 to UTF-16");
+		zval_ptr_dtor( zstr );
+		ZVAL_NULL( zstr );
+		return nullptr;
 	}

 	/* Cleanup zstr to hold utf16 string. */
@@ -339,7 +360,11 @@ U_CFUNC zend_string *collator_zval_to_string(zval *arg)
 		return zend_string_copy(Z_STR_P(arg));
 	}

-	zend_string *utf8_str = zval_get_string(arg);
+	zend_string *utf8_str = zval_try_get_string(arg);
+	if (utf8_str == nullptr) {
+		return nullptr;
+	}
+
 	zend_string *utf16_str = collator_convert_zstr_utf8_to_utf16(utf8_str);
 	zend_string_release(utf8_str);
 	return utf16_str;
diff --git a/ext/intl/collator/collator_sort.cpp b/ext/intl/collator/collator_sort.cpp
index b7c2b873659..cb1f2aefc35 100644
--- a/ext/intl/collator/collator_sort.cpp
+++ b/ext/intl/collator/collator_sort.cpp
@@ -50,6 +50,17 @@ static const size_t DEF_SORT_KEYS_INDX_BUF_INCREMENT = 1048576;

 static const size_t DEF_UTF16_BUF_SIZE = 1024;

+static void collator_report_sort_error(Collator_object *co, const intl_error *sort_error)
+{
+	if (sort_error->custom_error_message) {
+		intl_errors_set(
+			COLLATOR_ERROR_P(co), sort_error->code,
+			ZSTR_VAL(sort_error->custom_error_message));
+	} else {
+		intl_errors_set_code(COLLATOR_ERROR_P(co), sort_error->code);
+	}
+}
+
 /* {{{ collator_regular_compare_function */
 static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
 {
@@ -59,12 +70,20 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
 	zval norm1, norm2;
 	zval *num1_p = nullptr, *num2_p = nullptr;
 	zval *norm1_p = nullptr, *norm2_p = nullptr;
-	zval *str1_p, *str2_p;
+	zval *str1_p = nullptr, *str2_p = nullptr;

 	ZVAL_NULL(&str1);
 	str1_p  = collator_convert_object_to_string( op1, &str1 );
+	if( str1_p == nullptr ) {
+		return FAILURE;
+	}
+
 	ZVAL_NULL(&str2);
 	str2_p  = collator_convert_object_to_string( op2, &str2 );
+	if( str2_p == nullptr ) {
+		rc = FAILURE;
+		goto cleanup;
+	}

 	/* If both args are strings AND either of args is not numeric string
 	 * then use ICU-compare. Otherwise PHP-compare. */
@@ -90,9 +109,17 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
 				 * just convert it to utf8.
 				 */
 				norm1_p = collator_convert_zstr_utf16_to_utf8( str1_p, &norm1 );
+				if( norm1_p == nullptr ) {
+					rc = FAILURE;
+					goto cleanup;
+				}

 				/* num2 is not set but str2 is string => do normalization. */
 				norm2_p = collator_normalize_sort_argument( str2_p, &norm2 );
+				if( norm2_p == nullptr ) {
+					rc = FAILURE;
+					goto cleanup;
+				}
 			}
 			else
 			{
@@ -109,16 +136,28 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
 		{
 			/* num1 is not set if str1 or str2 is not a string => do normalization. */
 			norm1_p = collator_normalize_sort_argument( str1_p, &norm1 );
+			if( norm1_p == nullptr ) {
+				rc = FAILURE;
+				goto cleanup;
+			}

 			/* if num1 is not set then num2 is not set as well => do normalization. */
 			norm2_p = collator_normalize_sort_argument( str2_p, &norm2 );
+			if( norm2_p == nullptr ) {
+				rc = FAILURE;
+				goto cleanup;
+			}
 		}

 		rc = compare_function( result, norm1_p, norm2_p );
+	}

+cleanup:
+	if( norm1_p )
 		zval_ptr_dtor( norm1_p );
+
+	if( norm2_p )
 		zval_ptr_dtor( norm2_p );
-	}

 	if( num1_p )
 		zval_ptr_dtor( num1_p );
@@ -126,8 +165,11 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
 	if( num2_p )
 		zval_ptr_dtor( num2_p );

-	zval_ptr_dtor( str1_p );
-	zval_ptr_dtor( str2_p );
+	if( str1_p )
+		zval_ptr_dtor( str1_p );
+
+	if( str2_p )
+		zval_ptr_dtor( str2_p );

 	return rc;
 }
@@ -170,9 +212,16 @@ static int collator_numeric_compare_function(zval *result, zval *op1, zval *op2)
 */
 static int collator_icu_compare_function(zval *result, zval *op1, zval *op2)
 {
-	int rc = SUCCESS;
 	zend_string *str1 = collator_zval_to_string(op1);
+	if( str1 == nullptr ) {
+		return FAILURE;
+	}
+
 	zend_string *str2 = collator_zval_to_string(op2);
+	if( str2 == nullptr ) {
+		zend_string_release(str1);
+		return FAILURE;
+	}

 	/* Compare the strings using ICU. */
 	ZEND_ASSERT(INTL_G(current_collator) != nullptr);
@@ -184,7 +233,7 @@ static int collator_icu_compare_function(zval *result, zval *op1, zval *op2)
 	zend_string_release(str1);
 	zend_string_release(str2);

-	return rc;
+	return SUCCESS;
 }
 /* }}} */

@@ -197,6 +246,11 @@ static int collator_compare_func(Bucket *f, Bucket *s)
 	zval *first = &f->val;
 	zval *second = &s->val;

+	ZEND_ASSERT(INTL_G(current_collator_error) != nullptr);
+	if( EG(exception) || U_FAILURE( INTL_ERROR_CODE(*INTL_G(current_collator_error)) ) ) {
+		return 0;
+	}
+
 	if( INTL_G(compare_func)( &result, first, second) == FAILURE )
 		return 0;

@@ -260,6 +314,9 @@ static collator_compare_func_t collator_get_compare_function( const zend_long so
 static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
 {
 	UCollator*     saved_collator;
+	intl_error*    saved_collator_error;
+	intl_error     sort_error;
+	collator_compare_func_t saved_compare_func;
 	zval*          array            = nullptr;
 	HashTable*     hash             = nullptr;
 	zend_array*    sorted           = nullptr;
@@ -282,9 +339,6 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
 		RETURN_THROWS();
 	}

-	/* Set 'compare function' according to sort flags. */
-	INTL_G(compare_func) = collator_get_compare_function( sort_flags );
-
 	hash = Z_ARRVAL_P( array );

 	/* Copy array, so the in-place modifications will not be visible to the callback function */
@@ -297,15 +351,38 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
 	}
 	COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-8 to UTF-16" );

+	intl_error_init( &sort_error );
+
 	/* Save specified collator in the request-global (?) variable. */
 	saved_collator = INTL_G( current_collator );
+	saved_collator_error = INTL_G( current_collator_error );
+	saved_compare_func = INTL_G( compare_func );
 	INTL_G( current_collator ) = co->ucoll;
+	INTL_G( current_collator_error ) = &sort_error;
+	INTL_G( compare_func ) = collator_get_compare_function( sort_flags );

 	/* Sort specified array. */
 	zend_hash_sort( sorted, collator_compare_func, renumber );

 	/* Restore saved collator. */
 	INTL_G( current_collator ) = saved_collator;
+	INTL_G( current_collator_error ) = saved_collator_error;
+	INTL_G( compare_func ) = saved_compare_func;
+
+	if( EG(exception) ) {
+		zend_array_destroy( sorted );
+		intl_error_reset( &sort_error );
+		RETURN_THROWS();
+	}
+
+	if( U_FAILURE( INTL_ERROR_CODE(sort_error) ) ) {
+		zend_array_destroy( sorted );
+		collator_report_sort_error(co, &sort_error);
+		intl_error_reset( &sort_error );
+		RETURN_FALSE;
+	}
+
+	intl_error_reset( &sort_error );

 	/* Convert strings in the specified array back to UTF-8. */
 	collator_convert_hash_from_utf16_to_utf8( sorted, COLLATOR_ERROR_CODE_P( co ) );
diff --git a/ext/intl/php_intl.c b/ext/intl/php_intl.c
index 697d77a98bf..7bc599728e6 100644
--- a/ext/intl/php_intl.c
+++ b/ext/intl/php_intl.c
@@ -278,6 +278,7 @@ PHP_RINIT_FUNCTION( intl )
 PHP_RSHUTDOWN_FUNCTION( intl )
 {
 	INTL_G(current_collator) = NULL;
+	INTL_G(current_collator_error) = NULL;
 	if (INTL_G(grapheme_iterator)) {
 		grapheme_close_global_iterator(  );
 		INTL_G(grapheme_iterator) = NULL;
diff --git a/ext/intl/php_intl.h b/ext/intl/php_intl.h
index 1a9b4f769e8..6b6cce59b92 100644
--- a/ext/intl/php_intl.h
+++ b/ext/intl/php_intl.h
@@ -45,6 +45,7 @@ extern zend_module_entry intl_module_entry;

 ZEND_BEGIN_MODULE_GLOBALS(intl)
 	struct UCollator *current_collator;
+	intl_error *current_collator_error;
 	char* default_locale;
 	collator_compare_func_t compare_func;
 	UBreakIterator* grapheme_iterator;
diff --git a/ext/intl/tests/collator_sort_conversion_error.phpt b/ext/intl/tests/collator_sort_conversion_error.phpt
new file mode 100644
index 00000000000..acc62df0704
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Collator::sort() reports conversion errors from comparison callbacks
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+$array = ['b', new BadString(), 'a'];
+
+var_dump($coll->sort($array, Collator::SORT_STRING));
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), "\n";
+?>
+--EXPECT--
+bool(false)
+bool(true)
+Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
diff --git a/ext/intl/tests/collator_sort_conversion_error_exception.phpt b/ext/intl/tests/collator_sort_conversion_error_exception.phpt
new file mode 100644
index 00000000000..c9a0e29694f
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error_exception.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Collator::sort() throws IntlException on conversion errors with intl.use_exceptions
+--EXTENSIONS--
+intl
+--INI--
+intl.use_exceptions=1
+--FILE--
+<?php
+class BadString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+
+$array = ['b', new BadString(), 'a'];
+try {
+    $coll->sort($array, Collator::SORT_STRING);
+    echo 'no exception', PHP_EOL;
+} catch (IntlException $e) {
+    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
+}
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+var_dump($array[0], $array[1] instanceof BadString, $array[2]);
+
+$array = ['b' => 'b', 'bad' => new BadString(), 'a' => 'a'];
+try {
+    collator_asort($coll, $array, Collator::SORT_STRING);
+    echo 'no exception', PHP_EOL;
+} catch (IntlException $e) {
+    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
+}
+var_dump(array_keys($array));
+?>
+--EXPECT--
+IntlException: Collator::sort(): Error converting string from UTF-8 to UTF-16
+bool(true)
+string(1) "b"
+bool(true)
+string(1) "a"
+IntlException: collator_asort(): Error converting string from UTF-8 to UTF-16
+array(3) {
+  [0]=>
+  string(1) "b"
+  [1]=>
+  string(3) "bad"
+  [2]=>
+  string(1) "a"
+}
diff --git a/ext/intl/tests/collator_sort_conversion_error_level.phpt b/ext/intl/tests/collator_sort_conversion_error_level.phpt
new file mode 100644
index 00000000000..adcf96d1ed7
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error_level.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Collator::sort() reports comparison conversion errors through intl.error_level
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadErrorLevelString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+ini_set("intl.error_level", E_WARNING);
+ini_set("intl.use_exceptions", 0);
+
+$coll = new Collator('en_US');
+$array = ['b', new BadErrorLevelString(), 'a'];
+
+var_dump($coll->sort($array, Collator::SORT_STRING));
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), PHP_EOL;
+?>
+--EXPECTF--
+Deprecated: ini_set(): Using a value different than 0 for intl.error_level is deprecated, as the intl.error_level INI setting is deprecated. Instead the intl.use_exceptions INI setting should be enabled to throw exceptions on errors or intl_get_error_code()/intl_get_error_message() should be used to manually deal with errors in %s on line %d
+
+Warning: Collator::sort(): Error converting string from UTF-8 to UTF-16 in %s on line %d
+bool(false)
+bool(true)
+Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
diff --git a/ext/intl/tests/collator_sort_conversion_error_message_sites.phpt b/ext/intl/tests/collator_sort_conversion_error_message_sites.phpt
new file mode 100644
index 00000000000..b300c4e7499
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error_message_sites.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Collator::sort() conversion error messages describe the failed conversion
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+
+$array = ['a', new BadString()];
+var_dump($coll->sort($array, Collator::SORT_REGULAR));
+echo $coll->getErrorMessage(), PHP_EOL;
+
+$array = ['a', new BadString()];
+var_dump($coll->sort($array, Collator::SORT_STRING));
+echo $coll->getErrorMessage(), PHP_EOL;
+?>
+--EXPECT--
+bool(false)
+Collator::sort(): Error converting object string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(false)
+Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
diff --git a/ext/intl/tests/collator_sort_conversion_error_procedural.phpt b/ext/intl/tests/collator_sort_conversion_error_procedural.phpt
new file mode 100644
index 00000000000..e7be7806f63
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error_procedural.phpt
@@ -0,0 +1,54 @@
+--TEST--
+collator_sort() and collator_asort() report conversion errors
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadProceduralString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+$coll = collator_create('en_US');
+
+$array = ['b', new BadProceduralString(), 'a'];
+var_dump(collator_sort($coll, $array, Collator::SORT_STRING));
+var_dump($array[0]);
+var_dump($array[1] instanceof BadProceduralString);
+var_dump($array[2]);
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump(collator_get_error_code($coll) === U_INVALID_CHAR_FOUND);
+echo collator_get_error_message($coll), "\n";
+
+$array = ['b' => 'b', 'bad' => new BadProceduralString(), 'a' => 'a'];
+var_dump(collator_asort($coll, $array, Collator::SORT_STRING));
+var_dump(array_keys($array));
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump(collator_get_error_code($coll) === U_INVALID_CHAR_FOUND);
+echo collator_get_error_message($coll), "\n";
+?>
+--EXPECT--
+bool(false)
+string(1) "b"
+bool(true)
+string(1) "a"
+bool(true)
+collator_sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+collator_sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(false)
+array(3) {
+  [0]=>
+  string(1) "b"
+  [1]=>
+  string(3) "bad"
+  [2]=>
+  string(1) "a"
+}
+bool(true)
+collator_asort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+collator_asort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
diff --git a/ext/intl/tests/collator_sort_conversion_error_regular.phpt b/ext/intl/tests/collator_sort_conversion_error_regular.phpt
new file mode 100644
index 00000000000..439d9293d5d
--- /dev/null
+++ b/ext/intl/tests/collator_sort_conversion_error_regular.phpt
@@ -0,0 +1,70 @@
+--TEST--
+Collator::sort() SORT_REGULAR conversion error cleanup paths
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadRegularString {
+    public function __toString(): string {
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+
+$array = [new BadRegularString(), 1];
+var_dump($coll->sort($array, Collator::SORT_REGULAR));
+var_dump($array[0] instanceof BadRegularString);
+var_dump($array[1]);
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), "\n";
+
+$array = ['a', new BadRegularString()];
+var_dump($coll->sort($array, Collator::SORT_REGULAR));
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+var_dump($array[0], $array[1] instanceof BadRegularString);
+
+$array = ['10', '9'];
+var_dump($coll->sort($array, Collator::SORT_REGULAR));
+var_dump($coll->getErrorCode() === U_ZERO_ERROR);
+var_dump($array);
+
+$array = ['b', '9'];
+var_dump($coll->sort($array, Collator::SORT_REGULAR));
+var_dump($coll->getErrorCode() === U_ZERO_ERROR);
+var_dump($array);
+?>
+--EXPECT--
+bool(false)
+bool(true)
+int(1)
+bool(true)
+Collator::sort(): Error converting object string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+Collator::sort(): Error converting object string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(false)
+bool(true)
+Collator::sort(): Error converting object string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+string(1) "a"
+bool(true)
+bool(true)
+bool(true)
+array(2) {
+  [0]=>
+  string(1) "9"
+  [1]=>
+  string(2) "10"
+}
+bool(true)
+bool(true)
+array(2) {
+  [0]=>
+  string(1) "9"
+  [1]=>
+  string(1) "b"
+}
diff --git a/ext/intl/tests/collator_sort_error_reset_reentrancy.phpt b/ext/intl/tests/collator_sort_error_reset_reentrancy.phpt
new file mode 100644
index 00000000000..ac218e27693
--- /dev/null
+++ b/ext/intl/tests/collator_sort_error_reset_reentrancy.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Collator::sort() conversion failure must survive a re-entrant Collator call in __toString()
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class BadAfterReentry {
+    public static Collator $coll;
+    public static bool $called = false;
+
+    public function __toString(): string {
+        self::$called = true;
+        self::$coll->getLocale(Locale::VALID_LOCALE);
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+BadAfterReentry::$coll = $coll;
+
+$array = ['a', new BadAfterReentry(), 'b'];
+
+var_dump($coll->sort($array, Collator::SORT_STRING));
+var_dump(BadAfterReentry::$called);
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), PHP_EOL;
+var_dump($array[0], $array[1] instanceof BadAfterReentry, $array[2]);
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+string(1) "a"
+bool(true)
+string(1) "b"
diff --git a/ext/intl/tests/collator_sort_nested_compare_func.phpt b/ext/intl/tests/collator_sort_nested_compare_func.phpt
new file mode 100644
index 00000000000..bc08a466f79
--- /dev/null
+++ b/ext/intl/tests/collator_sort_nested_compare_func.phpt
@@ -0,0 +1,63 @@
+--TEST--
+Collator::sort() must not let a nested sort change the running comparator
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class Nest {
+    public static Collator $coll;
+    public static bool $done = false;
+
+    public function __toString(): string {
+        if (!self::$done) {
+            self::$done = true;
+            $inner = ['10', '9', '2'];
+            self::$coll->sort($inner, Collator::SORT_NUMERIC);
+        }
+        return 'm';
+    }
+}
+
+function names(array $a): array {
+    return array_map(static fn($v) => is_object($v) ? get_class($v) : $v, $a);
+}
+
+$coll = new Collator('en_US');
+Nest::$coll = $coll;
+
+$a = ['20', '3', new Nest(), '100', '9'];
+var_dump($coll->sort($a, Collator::SORT_STRING));
+var_dump(names($a));
+
+Nest::$done = true;
+$b = ['20', '3', new Nest(), '100', '9'];
+var_dump($coll->sort($b, Collator::SORT_STRING));
+var_dump(names($b));
+?>
+--EXPECT--
+bool(true)
+array(5) {
+  [0]=>
+  string(3) "100"
+  [1]=>
+  string(2) "20"
+  [2]=>
+  string(1) "3"
+  [3]=>
+  string(1) "9"
+  [4]=>
+  string(4) "Nest"
+}
+bool(true)
+array(5) {
+  [0]=>
+  string(3) "100"
+  [1]=>
+  string(2) "20"
+  [2]=>
+  string(1) "3"
+  [3]=>
+  string(1) "9"
+  [4]=>
+  string(4) "Nest"
+}
diff --git a/ext/intl/tests/collator_sort_numeric.phpt b/ext/intl/tests/collator_sort_numeric.phpt
new file mode 100644
index 00000000000..33f000c10eb
--- /dev/null
+++ b/ext/intl/tests/collator_sort_numeric.phpt
@@ -0,0 +1,83 @@
+--TEST--
+Collator::sort() and asort() SORT_NUMERIC paths
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+$coll = new Collator('en_US');
+
+$array = ['10', '-3', '4.5', '2'];
+var_dump($coll->sort($array, Collator::SORT_NUMERIC));
+var_dump($coll->getErrorCode() === U_ZERO_ERROR);
+var_dump($array);
+
+$array = ['ten' => '10', 'minus' => '-3', 'four' => '4.5', 'two' => '2'];
+var_dump($coll->asort($array, Collator::SORT_NUMERIC));
+var_dump($coll->getErrorCode() === U_ZERO_ERROR);
+var_dump(array_keys($array));
+
+$array = ["\xFF", '10', '1'];
+var_dump($coll->sort($array, Collator::SORT_NUMERIC));
+var_dump($array[0] === "\xFF");
+var_dump($array[1], $array[2]);
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), "\n";
+
+$array = ['ten' => '10', 'bad' => "\xFF", 'one' => '1'];
+var_dump($coll->asort($array, Collator::SORT_NUMERIC));
+var_dump(array_keys($array));
+var_dump($array['bad'] === "\xFF");
+var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
+echo intl_get_error_message(), "\n";
+var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+echo $coll->getErrorMessage(), "\n";
+?>
+--EXPECT--
+bool(true)
+bool(true)
+array(4) {
+  [0]=>
+  string(2) "-3"
+  [1]=>
+  string(1) "2"
+  [2]=>
+  string(3) "4.5"
+  [3]=>
+  string(2) "10"
+}
+bool(true)
+bool(true)
+array(4) {
+  [0]=>
+  string(5) "minus"
+  [1]=>
+  string(3) "two"
+  [2]=>
+  string(4) "four"
+  [3]=>
+  string(3) "ten"
+}
+bool(false)
+bool(true)
+string(2) "10"
+string(1) "1"
+bool(true)
+Collator::sort(): Error converting hash from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+Collator::sort(): Error converting hash from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(false)
+array(3) {
+  [0]=>
+  string(3) "ten"
+  [1]=>
+  string(3) "bad"
+  [2]=>
+  string(3) "one"
+}
+bool(true)
+bool(true)
+Collator::asort(): Error converting hash from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
+bool(true)
+Collator::asort(): Error converting hash from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
diff --git a/ext/intl/tests/collator_sort_stops_after_failure.phpt b/ext/intl/tests/collator_sort_stops_after_failure.phpt
new file mode 100644
index 00000000000..b9166e6abb8
--- /dev/null
+++ b/ext/intl/tests/collator_sort_stops_after_failure.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Collator::sort() must stop comparing once a conversion has failed
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class CountingBad {
+    public static int $calls = 0;
+
+    public function __toString(): string {
+        self::$calls++;
+        return "\xFF";
+    }
+}
+
+$coll = new Collator('en_US');
+
+foreach ([Collator::SORT_STRING, Collator::SORT_REGULAR] as $flag) {
+    CountingBad::$calls = 0;
+    $array = ['f', 'e', 'd', new CountingBad(), 'c', 'b', 'a'];
+    var_dump($coll->sort($array, $flag));
+    var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
+    var_dump(CountingBad::$calls);
+}
+?>
+--EXPECT--
+bool(false)
+bool(true)
+int(1)
+bool(false)
+bool(true)
+int(1)
diff --git a/ext/intl/tests/collator_sort_tostring_throws.phpt b/ext/intl/tests/collator_sort_tostring_throws.phpt
new file mode 100644
index 00000000000..309c641af2a
--- /dev/null
+++ b/ext/intl/tests/collator_sort_tostring_throws.phpt
@@ -0,0 +1,67 @@
+--TEST--
+Collator::sort() must not reorder the array when __toString() fails
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+class Thrower {
+    public function __toString(): string {
+        throw new Exception('boom');
+    }
+}
+
+class NoToString {
+}
+
+function names(array $a): array {
+    return array_map(static fn($v) => is_object($v) ? get_class($v) : $v, $a);
+}
+
+$coll = new Collator('en_US');
+
+foreach ([Collator::SORT_REGULAR, Collator::SORT_STRING] as $flag) {
+    $array = ['b', new Thrower(), 'a'];
+    try {
+        var_dump($coll->sort($array, $flag));
+    } catch (Throwable $e) {
+        echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
+    }
+    var_dump(names($array));
+}
+
+$array = ['b', new NoToString(), 'a'];
+try {
+    var_dump($coll->sort($array, Collator::SORT_STRING));
+} catch (Throwable $e) {
+    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
+}
+var_dump(names($array));
+?>
+--EXPECT--
+Exception: boom
+array(3) {
+  [0]=>
+  string(1) "b"
+  [1]=>
+  string(7) "Thrower"
+  [2]=>
+  string(1) "a"
+}
+Exception: boom
+array(3) {
+  [0]=>
+  string(1) "b"
+  [1]=>
+  string(7) "Thrower"
+  [2]=>
+  string(1) "a"
+}
+Error: Object of class NoToString could not be converted to string
+array(3) {
+  [0]=>
+  string(1) "b"
+  [1]=>
+  string(10) "NoToString"
+  [2]=>
+  string(1) "a"
+}