Commit 5fa74db18d9 for php.net
commit 5fa74db18d9220b72cc6a04072e317e695f23c90
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jun 26 08:15:14 2026 -0400
Fix use-after-free in Collator::sort() with a mutating comparator
collator_sort() and collator_asort() sort the array in place while the
comparator may run a __toString() that appends to the same array through
a reference, reallocating its storage under the running sort. Sort a copy
and swap it back, matching usort().
Closes GH-22467
diff --git a/ext/intl/collator/collator_sort.c b/ext/intl/collator/collator_sort.c
index 47e48624e6b..9d4cb422020 100644
--- a/ext/intl/collator/collator_sort.c
+++ b/ext/intl/collator/collator_sort.c
@@ -259,12 +259,13 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
UCollator* saved_collator;
zval* array = NULL;
HashTable* hash = NULL;
+ zend_array* sorted = NULL;
zend_long sort_flags = COLLATOR_SORT_REGULAR;
COLLATOR_METHOD_INIT_VARS
/* Parse parameters. */
- if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Oa/|l",
+ if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Oa|l",
&object, Collator_ce_ptr, &array, &sort_flags ) == FAILURE )
{
RETURN_THROWS();
@@ -283,8 +284,14 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
hash = Z_ARRVAL_P( array );
+ /* Copy array, so the in-place modifications will not be visible to the callback function */
+ sorted = zend_array_dup( hash );
+
/* Convert strings in the specified array from UTF-8 to UTF-16. */
- collator_convert_hash_from_utf8_to_utf16( hash, COLLATOR_ERROR_CODE_P( co ) );
+ collator_convert_hash_from_utf8_to_utf16( sorted, COLLATOR_ERROR_CODE_P( co ) );
+ if( U_FAILURE( COLLATOR_ERROR_CODE( co ) ) ) {
+ zend_array_destroy( sorted );
+ }
COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-8 to UTF-16" );
/* Save specified collator in the request-global (?) variable. */
@@ -292,15 +299,23 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
INTL_G( current_collator ) = co->ucoll;
/* Sort specified array. */
- zend_hash_sort(hash, collator_compare_func, renumber);
+ zend_hash_sort( sorted, collator_compare_func, renumber );
/* Restore saved collator. */
INTL_G( current_collator ) = saved_collator;
/* Convert strings in the specified array back to UTF-8. */
- collator_convert_hash_from_utf16_to_utf8( hash, COLLATOR_ERROR_CODE_P( co ) );
+ collator_convert_hash_from_utf16_to_utf8( sorted, COLLATOR_ERROR_CODE_P( co ) );
+ if( U_FAILURE( COLLATOR_ERROR_CODE( co ) ) ) {
+ zend_array_destroy( sorted );
+ }
COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-16 to UTF-8" );
+ zval garbage;
+ ZVAL_COPY_VALUE( &garbage, array );
+ ZVAL_ARR( array, sorted );
+ zval_ptr_dtor( &garbage );
+
RETURN_TRUE;
}
/* }}} */
diff --git a/ext/intl/tests/collator_sort_modify_during_compare.phpt b/ext/intl/tests/collator_sort_modify_during_compare.phpt
new file mode 100644
index 00000000000..427f5833f1f
--- /dev/null
+++ b/ext/intl/tests/collator_sort_modify_during_compare.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Collator::sort(): mutating the array from __toString() during comparison must not corrupt the sort
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+$c = new Collator('en_US');
+
+class Grow {
+ public static array $ref;
+ public function __toString(): string {
+ for ($i = 0; $i < 2000; $i++) {
+ self::$ref[] = "x$i";
+ }
+ return "m";
+ }
+}
+
+$arr = ["z", new Grow(), "a", "b"];
+Grow::$ref = &$arr;
+var_dump($c->sort($arr));
+var_dump($arr);
+?>
+--EXPECT--
+bool(true)
+array(4) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ string(1) "b"
+ [2]=>
+ object(Grow)#2 (0) {
+ }
+ [3]=>
+ string(1) "z"
+}