Skip to content

Commit 05bd142

Browse files
committed
Fix GH-11189: Exceeding memory limit in zend_hash_do_resize leaves the array in an invalid state
There are more places in zend_hash.c where the resize happened after some values on the HashTable struct were set. I reordered them all, but writing a test for these would rely on the particular amount of bytes allocated at given points in time.
1 parent 81e50b4 commit 05bd142

File tree

4 files changed

+70
-7
lines changed

4 files changed

+70
-7
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ PHP NEWS
55
- Core:
66
. Fixed bug GH-9068 (Conditional jump or move depends on uninitialised
77
value(s)). (nielsdos)
8+
. Fixed bug GH-11189 (Exceeding memory limit in zend_hash_do_resize leaves
9+
the array in an invalid state). (Bob)
810

911
- Opcache:
1012
. Fixed bug GH-11134 (Incorrect match default branch optimization). (ilutov)

Zend/tests/gh11189.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
GH-11189: Exceeding memory limit in zend_hash_do_resize leaves the array in an invalid state (packed array)
3+
--SKIPIF--
4+
<?php
5+
if (getenv("USE_ZEND_ALLOC") === "0") die("skip ZMM is disabled");
6+
?>
7+
--INI--
8+
memory_limit=2M
9+
--FILE--
10+
<?php
11+
12+
ob_start(function() {
13+
global $a;
14+
for ($i = count($a); $i > 0; --$i) {
15+
$a[] = 2;
16+
}
17+
fwrite(STDOUT, "Success");
18+
});
19+
20+
$a = [];
21+
// trigger OOM in a resize operation
22+
while (1) {
23+
$a[] = 1;
24+
}
25+
26+
?>
27+
--EXPECTF--
28+
Success
29+
Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d

Zend/tests/gh11189_1.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
GH-11189: Exceeding memory limit in zend_hash_do_resize leaves the array in an invalid state (not packed array)
3+
--SKIPIF--
4+
<?php
5+
if (getenv("USE_ZEND_ALLOC") === "0") die("skip ZMM is disabled");
6+
?>
7+
--INI--
8+
memory_limit=2M
9+
--FILE--
10+
<?php
11+
12+
ob_start(function() {
13+
global $a;
14+
for ($i = count($a); $i > 0; --$i) {
15+
$a[] = 2;
16+
}
17+
fwrite(STDOUT, "Success");
18+
});
19+
20+
$a = ["not packed" => 1];
21+
// trigger OOM in a resize operation
22+
while (1) {
23+
$a[] = 1;
24+
}
25+
26+
?>
27+
--EXPECTF--
28+
Success
29+
Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d

Zend/zend_hash.c

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,9 @@ ZEND_API void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht)
309309
if (ht->nTableSize >= HT_MAX_SIZE) {
310310
zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%u * %zu + %zu)", ht->nTableSize * 2, sizeof(Bucket), sizeof(Bucket));
311311
}
312-
ht->nTableSize += ht->nTableSize;
313-
HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_SIZE_EX(ht->nTableSize, HT_MIN_MASK), HT_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
312+
uint32_t newTableSize = ht->nTableSize * 2;
313+
HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_SIZE_EX(newTableSize, HT_MIN_MASK), HT_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
314+
ht->nTableSize = newTableSize;
314315
}
315316

316317
ZEND_API void ZEND_FASTCALL zend_hash_real_init(HashTable *ht, bool packed)
@@ -346,8 +347,9 @@ ZEND_API void ZEND_FASTCALL zend_hash_packed_to_hash(HashTable *ht)
346347
ZEND_ASSERT(HT_SIZE_TO_MASK(nSize));
347348

348349
HT_ASSERT_RC1(ht);
349-
HT_FLAGS(ht) &= ~HASH_FLAG_PACKED;
350+
// Alloc before assign to avoid inconsistencies on OOM
350351
new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
352+
HT_FLAGS(ht) &= ~HASH_FLAG_PACKED;
351353
ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
352354
HT_SET_DATA_ADDR(ht, new_data);
353355
memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);
@@ -387,17 +389,18 @@ ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool
387389
if (packed) {
388390
ZEND_ASSERT(HT_FLAGS(ht) & HASH_FLAG_PACKED);
389391
if (nSize > ht->nTableSize) {
390-
ht->nTableSize = zend_hash_check_size(nSize);
391-
HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_SIZE_EX(ht->nTableSize, HT_MIN_MASK), HT_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
392+
uint32_t newTableSize = zend_hash_check_size(nSize);
393+
HT_SET_DATA_ADDR(ht, perealloc2(HT_GET_DATA_ADDR(ht), HT_SIZE_EX(newTableSize, HT_MIN_MASK), HT_USED_SIZE(ht), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT));
394+
ht->nTableSize = newTableSize;
392395
}
393396
} else {
394397
ZEND_ASSERT(!(HT_FLAGS(ht) & HASH_FLAG_PACKED));
395398
if (nSize > ht->nTableSize) {
396399
void *new_data, *old_data = HT_GET_DATA_ADDR(ht);
397400
Bucket *old_buckets = ht->arData;
398401
nSize = zend_hash_check_size(nSize);
399-
ht->nTableSize = nSize;
400402
new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
403+
ht->nTableSize = nSize;
401404
ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
402405
HT_SET_DATA_ADDR(ht, new_data);
403406
memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);
@@ -1217,8 +1220,8 @@ static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)
12171220

12181221
ZEND_ASSERT(HT_SIZE_TO_MASK(nSize));
12191222

1220-
ht->nTableSize = nSize;
12211223
new_data = pemalloc(HT_SIZE_EX(nSize, HT_SIZE_TO_MASK(nSize)), GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
1224+
ht->nTableSize = nSize;
12221225
ht->nTableMask = HT_SIZE_TO_MASK(ht->nTableSize);
12231226
HT_SET_DATA_ADDR(ht, new_data);
12241227
memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);

0 commit comments

Comments
 (0)