From 1c880960cd4f9f2ed060e58009dc146487cf5542 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 17 Aug 2026 22:42:21 +0100 Subject: [PATCH 1/2] 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. --- ext/standard/array.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; From a0594d53fac1d0e07028a9099812cf951358483f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 17 Aug 2026 22:45:38 +0100 Subject: [PATCH 2/2] add test --- .../array_merge_recursive_object_leak.phpt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 ext/standard/tests/array/array_merge_recursive_object_leak.phpt 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-- + (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" + } +}