[php-src] PHP-8.4: ext/standard: array_merge_recursive() fix leak.

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-08-18T12:28:26+01:00

Commit: https://github.com/php/php-src/commit/4c71b4e58e37e8da533e5d86686e42fb0c70b019
Raw diff: https://github.com/php/php-src/commit/4c71b4e58e37e8da533e5d86686e42fb0c70b019.diff

ext/standard: array_merge_recursive() fix leak.

object to array conversion failure leaked the temporary zval.
while at it, fix reverse expectation on a failed neighbour insertion.

Close GH-23340

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


Diff:

diff --git a/ext/standard/array.c b/ext/standard/array.c
index 85a017eff7f9..556f9a7ee666 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -4133,12 +4133,13 @@ PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src) /* {{{ */
 						GC_TRY_UNPROTECT_RECURSION(thash);
 					}
 					if (!ret) {
+						zval_ptr_dtor(&tmp);
 						return 0;
 					}
 				} else {
 					Z_TRY_ADDREF_P(src_zval);
 					zval *zv = zend_hash_next_index_insert(Z_ARRVAL_P(dest_zval), src_zval);
-					if (EXPECTED(!zv)) {
+					if (UNEXPECTED(!zv)) {
 						Z_TRY_DELREF_P(src_zval);
 						zend_cannot_add_element();
 						return 0;
diff --git a/ext/standard/tests/array/array_merge_recursive_object_leak.phpt b/ext/standard/tests/array/array_merge_recursive_object_leak.phpt
new file mode 100644
index 000000000000..f4313057cf81
--- /dev/null
+++ b/ext/standard/tests/array/array_merge_recursive_object_leak.phpt
@@ -0,0 +1,55 @@
+--TEST--
+array_merge_recursive() must not leak the array converted from an object when the merge below it fails
+--FILE--
+<?php
+
+$dest = [];
+$dest['k'] = &$dest;
+try {
+    array_merge_recursive($dest, ['k' => (object) ['k' => 1]]);
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+/* Control: same failing exit, array source, nothing to release. */
+$control = [];
+$control['k'] = &$control;
+try {
+    array_merge_recursive($control, ['k' => ['k' => 1]]);
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+/* Several nested levels convert an object before the failure unwinds through them. */
+$ring = [[], [], []];
+for ($i = 0; $i < 3; $i++) {
+    $ring[$i]['k'] = &$ring[($i + 1) % 3];
+}
+$src = (object) ['k' => 1];
+for ($i = 1; $i < 3; $i++) {
+    $src = (object) ['k' => $src];
+}
+try {
+    array_merge_recursive($ring[0], ['k' => $src]);
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+/* The successful path still releases it exactly once. */
+$ok = ['k' => ['a']];
+var_dump(array_merge_recursive($ok, ['k' => (object) ['b']]));
+
+?>
+--EXPECT--
+Error: Recursion detected
+Error: Recursion detected
+Error: Recursion detected
+array(1) {
+  ["k"]=>
+  array(2) {
+    [0]=>
+    string(1) "a"
+    [1]=>
+    string(1) "b"
+  }
+}
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.