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

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-08-11T18:05:42+01:00

Commit: https://github.com/php/php-src/commit/281738e57fd4cf193c2cc99433cf59b00ea8dd9d
Raw diff: https://github.com/php/php-src/commit/281738e57fd4cf193c2cc99433cf59b00ea8dd9d.diff

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Fix GH-23204: use-after-free when __toString() destroys an array argument

Changed paths:
  A  ext/standard/tests/strings/gh23204.phpt
  M  ext/standard/string.c


Diff:

diff --git a/ext/standard/string.c b/ext/standard/string.c
index 54ed10c525f9..5e7b7d7c8921 100644
--- a/ext/standard/string.c
+++ b/ext/standard/string.c
@@ -971,6 +971,9 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
 
 	uint32_t flags = ZSTR_GET_COPYABLE_CONCAT_PROPERTIES(glue);
 
+	/* Converting an element may call __toString(), which can destroy pieces. */
+	GC_TRY_ADDREF(pieces);
+
 	ZEND_HASH_FOREACH_VAL(pieces, tmp) {
 		if (EXPECTED(Z_TYPE_P(tmp) == IS_STRING)) {
 			ptr->str = Z_STR_P(tmp);
@@ -1034,6 +1037,7 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
 	}
 
 	free_alloca(strings, use_heap);
+	GC_TRY_DTOR_NO_REF(pieces);
 	RETURN_NEW_STR(str);
 }
 /* }}} */
@@ -3399,7 +3403,12 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
 {
 	if (zend_hash_num_elements(from_ht) < 1) {
 		RETURN_STR_COPY(str);
-	} else if (zend_hash_num_elements(from_ht) == 1) {
+	}
+
+	/* Converting a replacement may call __toString(), which can destroy from_ht. */
+	GC_TRY_ADDREF(from_ht);
+
+	if (zend_hash_num_elements(from_ht) == 1) {
 		zend_long num_key;
 		zend_string *str_key, *tmp_str, *replace, *tmp_replace;
 		zval *entry;
@@ -3428,11 +3437,13 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
 			}
 			zend_tmp_string_release(tmp_str);
 			zend_tmp_string_release(tmp_replace);
-			return;
+			break;
 		} ZEND_HASH_FOREACH_END();
 	} else {
 		php_strtr_array_ex(return_value, str, from_ht);
 	}
+
+	GC_TRY_DTOR_NO_REF(from_ht);
 }
 
 /* {{{ Translates characters in str using given translation tables */
@@ -4492,6 +4503,17 @@ static void _php_str_replace_common(
 		RETURN_THROWS();
 	}
 
+	/* Converting an element may call __toString(), which can destroy the arrays. */
+	if (search_ht) {
+		GC_TRY_ADDREF(search_ht);
+	}
+	if (replace_ht) {
+		GC_TRY_ADDREF(replace_ht);
+	}
+	if (subject_ht) {
+		GC_TRY_ADDREF(subject_ht);
+	}
+
 	/* if subject is an array */
 	if (subject_ht) {
 		array_init(return_value);
@@ -4518,6 +4540,16 @@ static void _php_str_replace_common(
 	if (zcount) {
 		ZEND_TRY_ASSIGN_REF_LONG(zcount, count);
 	}
+
+	if (search_ht) {
+		GC_TRY_DTOR_NO_REF(search_ht);
+	}
+	if (replace_ht) {
+		GC_TRY_DTOR_NO_REF(replace_ht);
+	}
+	if (subject_ht) {
+		GC_TRY_DTOR_NO_REF(subject_ht);
+	}
 }
 
 /* {{{ php_str_replace_common */
diff --git a/ext/standard/tests/strings/gh23204.phpt b/ext/standard/tests/strings/gh23204.phpt
new file mode 100644
index 000000000000..e2ae20592c5a
--- /dev/null
+++ b/ext/standard/tests/strings/gh23204.phpt
@@ -0,0 +1,109 @@
+--TEST--
+GH-23204 (Use-after-free when __toString() destroys the array being read)
+--CREDITS--
+e1abrador
+--FILE--
+<?php
+class Unset_ implements Stringable {
+    public function __toString(): string {
+        global $a;
+        $a = null;
+        return "X";
+    }
+}
+
+$a = [new Unset_, 2, 3, 4];
+echo "destroyed: ", implode(",", $a), "\n";
+var_dump($a);
+
+class Append implements Stringable {
+    public function __toString(): string {
+        global $b;
+        $b[] = str_repeat("y", 32);
+        return "X";
+    }
+}
+
+$b = [new Append, 2, 3, 4];
+echo "appended: ", implode(",", $b), "\n";
+echo "count: ", count($b), "\n";
+
+class Boom implements Stringable {
+    public function __toString(): string {
+        global $c;
+        $c = null;
+        throw new Exception("boom");
+    }
+}
+
+$c = [new Boom, 2, 3, 4];
+try {
+    implode(",", $c);
+} catch (Exception $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+class UnsetPats implements Stringable {
+    public function __toString(): string {
+        global $d;
+        $d = null;
+        return "X";
+    }
+}
+
+$d = ["aa" => new UnsetPats, "bb" => "2", "cc" => "3", "dd" => "4"];
+echo "strtr: ", strtr("aabbccdd", $d), "\n";
+
+$e = ["aa" => new UnsetPats];
+$d = &$e;
+echo "strtr single: ", strtr("aabb", $e), "\n";
+
+class UnsetSearch implements Stringable {
+    public function __toString(): string {
+        global $f;
+        $f = null;
+        return "a";
+    }
+}
+
+$f = [new UnsetSearch, "b", "c", "d"];
+echo "str_replace search: ", str_replace($f, "z", "abcd"), "\n";
+
+class UnsetReplace implements Stringable {
+    public function __toString(): string {
+        global $g;
+        $g = null;
+        return "z";
+    }
+}
+
+$g = [new UnsetReplace, "y", "y", "y"];
+echo "str_replace replace: ", str_replace(["a", "b", "c", "d"], $g, "abcd"), "\n";
+
+class UnsetSubject implements Stringable {
+    public function __toString(): string {
+        global $h;
+        $h = null;
+        return "abcd";
+    }
+}
+
+$h = [new UnsetSubject, "abcd"];
+var_dump(str_replace("a", "z", $h));
+?>
+--EXPECT--
+destroyed: X,2,3,4
+NULL
+appended: X,2,3,4
+count: 5
+Exception: boom
+strtr: X234
+strtr single: Xbb
+str_replace search: zzzz
+str_replace replace: zyyy
+array(2) {
+  [0]=>
+  string(4) "zbcd"
+  [1]=>
+  string(4) "zbcd"
+}
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.