[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-12T20:37:35-04:00

Commit: https://github.com/php/php-src/commit/6493ea617aacb1e4b359511d65d961b842fd025e
Raw diff: https://github.com/php/php-src/commit/6493ea617aacb1e4b359511d65d961b842fd025e.diff

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Fix GH-22678: array_multisort() use-after-free on mutating comparator

Changed paths:
  A  ext/standard/tests/array/gh22678.phpt
  M  NEWS
  M  ext/standard/array.c


Diff:

diff --git a/NEWS b/NEWS
index 9cb1c52aacdd..46f23c3aa6d1 100644
--- a/NEWS
+++ b/NEWS
@@ -102,6 +102,8 @@ PHP                                                                        NEWS
 - Standard:
   . Fixed bug GH-22395 (base_convert() outputs at most 64 characters).
     (Weilin Du)
+  . Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator
+    mutates the array being sorted). (azchin, iliaal)
 
 - URI:
   . Fixed behavior of Uri\WhatWg\Url wither methods with regards to empty
diff --git a/ext/standard/array.c b/ext/standard/array.c
index b148bcf35689..bb5f43e405ea 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -6059,6 +6059,7 @@ PHP_FUNCTION(array_multisort)
 {
 	zval*			args;
 	zval**			arrays;
+	HashTable**		hashes;
 	Bucket**		indirect;
 	uint32_t		idx;
 	HashTable*		hash;
@@ -6180,11 +6181,17 @@ PHP_FUNCTION(array_multisort)
 	for (i = 0; i < array_size; i++) {
 		indirect[i] = indirects + (i * (num_arrays + 1));
 	}
+	hashes = safe_emalloc(num_arrays, sizeof(HashTable *), 0);
+	for (i = 0; i < num_arrays; i++) {
+		hashes[i] = Z_ARRVAL_P(arrays[i]);
+		GC_ADDREF(hashes[i]);
+		HT_ALLOW_COW_VIOLATION(hashes[i]);
+	}
 	for (i = 0; i < num_arrays; i++) {
 		k = 0;
-		if (HT_IS_PACKED(Z_ARRVAL_P(arrays[i]))) {
-			zval *zv = Z_ARRVAL_P(arrays[i])->arPacked;
-			for (idx = 0; idx < Z_ARRVAL_P(arrays[i])->nNumUsed; idx++, zv++) {
+		if (HT_IS_PACKED(hashes[i])) {
+			zval *zv = hashes[i]->arPacked;
+			for (idx = 0; idx < hashes[i]->nNumUsed; idx++, zv++) {
 				if (Z_TYPE_P(zv) == IS_UNDEF) continue;
 				ZVAL_COPY_VALUE(&indirect[k][i].val, zv);
 				indirect[k][i].h = idx;
@@ -6192,8 +6199,8 @@ PHP_FUNCTION(array_multisort)
 				k++;
 			}
 		} else {
-			Bucket *p = Z_ARRVAL_P(arrays[i])->arData;
-			for (idx = 0; idx < Z_ARRVAL_P(arrays[i])->nNumUsed; idx++, p++) {
+			Bucket *p = hashes[i]->arData;
+			for (idx = 0; idx < hashes[i]->nNumUsed; idx++, p++) {
 				if (Z_TYPE(p->val) == IS_UNDEF) continue;
 				indirect[k][i] = *p;
 				k++;
@@ -6213,7 +6220,7 @@ PHP_FUNCTION(array_multisort)
 
 	/* Restructure the arrays based on sorted indirect - this is mostly taken from zend_hash_sort() function. */
 	for (i = 0; i < num_arrays; i++) {
-		hash = Z_ARRVAL_P(arrays[i]);
+		hash = hashes[i];
 		hash->nNumUsed = array_size;
 		hash->nNextFreeElement = array_size;
 		hash->nInternalPointer = 0;
@@ -6242,6 +6249,14 @@ PHP_FUNCTION(array_multisort)
 	RETVAL_TRUE;
 
 clean_up:
+	for (i = 0; i < num_arrays; i++) {
+		if (UNEXPECTED(GC_DELREF(hashes[i]) == 0)) {
+			zend_array_destroy(hashes[i]);
+		} else {
+			gc_check_possible_root((zend_refcounted *)hashes[i]);
+		}
+	}
+	efree(hashes);
 	efree(indirects);
 	efree(indirect);
 	efree(func);
diff --git a/ext/standard/tests/array/gh22678.phpt b/ext/standard/tests/array/gh22678.phpt
new file mode 100644
index 000000000000..c1b71b592520
--- /dev/null
+++ b/ext/standard/tests/array/gh22678.phpt
@@ -0,0 +1,48 @@
+--TEST--
+GH-22678 (Use-after-free in array_multisort() when the comparator mutates the array)
+--FILE--
+<?php
+class Evil {
+    public function __toString(): string {
+        // Runs from the SORT_STRING comparator; drop the array being sorted.
+        foreach (array_keys($GLOBALS['a']) as $k) {
+            unset($GLOBALS['a'][$k]);
+        }
+        $GLOBALS['a'] = [];
+        return "x";
+    }
+}
+
+$a = [];
+for ($i = 0; $i < 10; $i++) {
+    $a[] = new Evil();
+}
+$GLOBALS['a'] = &$a;
+
+echo "freed mid-sort: ";
+var_dump(array_multisort($a, SORT_STRING));
+unset($GLOBALS['a']);
+
+// Non-packed integer keys reach the mixed-to-packed restructure while the sort
+// holds its reference on the array.
+$b = [5 => 'c', 3 => 'a', 1 => 'b'];
+array_multisort($b, SORT_STRING);
+echo "repacked: ", implode(',', $b), "\n";
+
+class Boom {
+    public function __toString(): string {
+        throw new Exception("boom");
+    }
+}
+
+$c = [new Boom(), new Boom()];
+try {
+    array_multisort($c, SORT_STRING);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+freed mid-sort: bool(true)
+repacked: a,b,c
+Exception: boom
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.