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