Skip to content

Commit f7b26a5

Browse files
committed
ext/standard: Fix array_merge() with an empty array argument
When one of the two arguments of array_merge() is empty, the other one is returned as is instead of being copied. The returned array is the argument itself, shared through copy-on-write, so it also kept the next free index of the argument. array_merge() renumbers integer keys from zero, so the next append to the result must use the element count. An argument whose next free index was stale, for example after unset() of its last element, made $result[] = $value use a wrong key. The shortcut now applies only when the argument already behaves like the result of the general code path: a packed array without holes, or an array with string keys only, whose next free index is the one the general path would produce. Everything else goes through the general path, which builds a fresh array. Clean arrays are still returned as is, so the copy is still avoided. array_merge_recursive() shares the same code path and had the same bug.
1 parent 6e8aad5 commit f7b26a5

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

‎ext/standard/array.c‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4275,15 +4275,20 @@ static zend_always_inline void php_array_merge_wrapper(INTERNAL_FUNCTION_PARAMET
42754275
ret = &args[0];
42764276
}
42774277
if (ret) {
4278-
if (HT_IS_PACKED(Z_ARRVAL_P(ret))) {
4279-
if (HT_IS_WITHOUT_HOLES(Z_ARRVAL_P(ret))) {
4278+
HashTable *ht = Z_ARRVAL_P(ret);
4279+
/* ZEND_LONG_MIN means no integer key yet, the next append uses index 0. */
4280+
zend_long next_free = ht->nNextFreeElement == ZEND_LONG_MIN ? 0 : ht->nNextFreeElement;
4281+
4282+
/* array_merge() renumbers integer keys from 0, so the next free index must match. */
4283+
if (HT_IS_PACKED(ht)) {
4284+
if (HT_IS_WITHOUT_HOLES(ht) && next_free == (zend_long) ht->nNumOfElements) {
42804285
RETURN_COPY(ret);
42814286
}
4282-
} else {
4287+
} else if (next_free == 0) {
42834288
bool copy = true;
42844289
zend_string *string_key;
42854290

4286-
ZEND_HASH_MAP_FOREACH_STR_KEY(Z_ARRVAL_P(ret), string_key) {
4291+
ZEND_HASH_MAP_FOREACH_STR_KEY(ht, string_key) {
42874292
if (!string_key) {
42884293
copy = false;
42894294
break;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
array_merge() with an empty argument returns an array with a fresh next free index
3+
--FILE--
4+
<?php
5+
// The result must behave like a fresh array: integer keys renumbered from 0.
6+
7+
$a = [1, 2, 3];
8+
unset($a[2]);
9+
$r = array_merge($a, []);
10+
$r[] = 4;
11+
var_dump($r);
12+
13+
$b = ['a' => 1];
14+
$b[] = 2;
15+
unset($b[0]);
16+
$r = array_merge([], $b);
17+
$r[] = 3;
18+
var_dump($r);
19+
?>
20+
--EXPECT--
21+
array(3) {
22+
[0]=>
23+
int(1)
24+
[1]=>
25+
int(2)
26+
[2]=>
27+
int(4)
28+
}
29+
array(2) {
30+
["a"]=>
31+
int(1)
32+
[0]=>
33+
int(3)
34+
}

0 commit comments

Comments
 (0)