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-- + (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" + } +}