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"
+}