Commit 4c71b4e58e3 for php.net

commit 4c71b4e58e37e8da533e5d86686e42fb0c70b019
Author: David Carlier <devnexen@gmail.com>
Date:   Tue Aug 18 12:28:26 2026 +0100

    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

diff --git a/ext/standard/array.c b/ext/standard/array.c
index 85a017eff7f..556f9a7ee66 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 00000000000..f4313057cf8
--- /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"
+  }
+}