Skip to content

Commit 216edef

Browse files
committed
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: Fix concurrent iteration and deletion issues in SplObjectStorage
2 parents 61ff154 + 43a4f91 commit 216edef

File tree

5 files changed

+148
-5
lines changed

5 files changed

+148
-5
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ PHP NEWS
3030
- SPL:
3131
. Fixed bug GH-21499 (RecursiveArrayIterator getChildren UAF after parent
3232
free). (Girgias)
33+
. Fix concurrent iteration and deletion issues in SplObjectStorage.
34+
(ndossche)
3335

3436
- Streams:
3537
. Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL

ext/spl/spl_observer.c

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,25 @@ static zend_result spl_object_storage_detach(spl_SplObjectStorage *intern, zend_
241241
return ret;
242242
} /* }}}*/
243243

244+
/* TODO: make this an official Zend API? */
245+
#define SPL_SAFE_HASH_FOREACH_PTR(_ht, _ptr) do { \
246+
const HashTable *__ht = (_ht); \
247+
zval *_z = __ht->arPacked; \
248+
for (uint32_t _idx = 0; _idx < __ht->nNumUsed; _idx++, _z = ZEND_HASH_ELEMENT(__ht, _idx)) { \
249+
if (UNEXPECTED(Z_ISUNDEF_P(_z))) continue; \
250+
_ptr = Z_PTR_P(_z);
251+
244252
static void spl_object_storage_addall(spl_SplObjectStorage *intern, spl_SplObjectStorage *other) { /* {{{ */
245253
spl_SplObjectStorageElement *element;
246254

247-
ZEND_HASH_FOREACH_PTR(&other->storage, element) {
248-
spl_object_storage_attach(intern, element->obj, &element->inf);
255+
SPL_SAFE_HASH_FOREACH_PTR(&other->storage, element) {
256+
zval zv;
257+
zend_object *obj = element->obj;
258+
GC_ADDREF(obj);
259+
ZVAL_COPY(&zv, &element->inf);
260+
spl_object_storage_attach(intern, obj, &zv);
261+
zval_ptr_dtor(&zv);
262+
OBJ_RELEASE(obj);
249263
} ZEND_HASH_FOREACH_END();
250264

251265
intern->index = 0;
@@ -626,10 +640,13 @@ PHP_METHOD(SplObjectStorage, removeAllExcept)
626640

627641
other = Z_SPLOBJSTORAGE_P(obj);
628642

629-
ZEND_HASH_FOREACH_PTR(&intern->storage, element) {
630-
if (!spl_object_storage_contains(other, element->obj)) {
631-
spl_object_storage_detach(intern, element->obj);
643+
SPL_SAFE_HASH_FOREACH_PTR(&intern->storage, element) {
644+
zend_object *elem_obj = element->obj;
645+
GC_ADDREF(elem_obj);
646+
if (!spl_object_storage_contains(other, elem_obj)) {
647+
spl_object_storage_detach(intern, elem_obj);
632648
}
649+
OBJ_RELEASE(elem_obj);
633650
} ZEND_HASH_FOREACH_END();
634651

635652
zend_hash_internal_pointer_reset_ex(&intern->storage, &intern->pos);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
SplObjectStorage: Concurrent deletion during iteration
3+
--CREDITS--
4+
cnitlrt
5+
--FILE--
6+
<?php
7+
class EvilStorage extends SplObjectStorage {
8+
public function getHash($obj): string {
9+
global $victim;
10+
static $counter = 0;
11+
if ($counter++ < 1024*2) {
12+
// Re-entrant mutation during removeAllExcept iteration
13+
for ($i = 0; $i < 5; $i++) {
14+
$victim[new stdClass()] = null;
15+
}
16+
}
17+
return spl_object_hash($obj);
18+
}
19+
}
20+
21+
$victim = new SplObjectStorage();
22+
$other = new EvilStorage();
23+
24+
for ($i = 0; $i < 1024; $i++) {
25+
$o = new stdClass();
26+
$victim[$o] = null;
27+
$other[$o] = null;
28+
}
29+
30+
var_dump($victim->removeAllExcept($other));
31+
32+
unset($victim, $other);
33+
$victim = new SplObjectStorage();
34+
$other = new EvilStorage();
35+
36+
for ($i = 0; $i < 1024; $i++) {
37+
$o = new stdClass();
38+
$victim[$o] = null;
39+
$other[$o] = null;
40+
}
41+
42+
var_dump($other->addAll($victim));
43+
?>
44+
--EXPECTF--
45+
int(%d)
46+
int(1024)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
SplObjectStorage: Concurrent deletion during addAll
3+
--CREDITS--
4+
cnitlrt
5+
--FILE--
6+
<?php
7+
8+
class EvilStorage extends SplObjectStorage {
9+
public function getHash($obj): string {
10+
global $storage;
11+
unset($storage[$obj]);
12+
return spl_object_hash($obj);
13+
}
14+
}
15+
16+
$storage = new SplObjectStorage();
17+
$storage[new stdClass] = 'foo';
18+
19+
$evil = new EvilStorage();
20+
$evil->addAll($storage);
21+
22+
var_dump($evil, $storage);
23+
24+
?>
25+
--EXPECTF--
26+
object(EvilStorage)#%d (1) {
27+
["storage":"SplObjectStorage":private]=>
28+
array(1) {
29+
[0]=>
30+
array(2) {
31+
["obj"]=>
32+
object(stdClass)#%d (0) {
33+
}
34+
["inf"]=>
35+
string(3) "foo"
36+
}
37+
}
38+
}
39+
object(SplObjectStorage)#%d (1) {
40+
["storage":"SplObjectStorage":private]=>
41+
array(0) {
42+
}
43+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
SplObjectStorage: Concurrent deletion during removeAllExcept
3+
--CREDITS--
4+
cnitlrt
5+
--FILE--
6+
<?php
7+
8+
class EvilStorage extends SplObjectStorage {
9+
public function getHash($obj): string {
10+
global $storage;
11+
unset($storage[$obj]);
12+
return spl_object_hash($obj);
13+
}
14+
}
15+
16+
$storage = new SplObjectStorage();
17+
$storage[new stdClass] = 'foo';
18+
19+
$evil = new EvilStorage();
20+
$storage->removeAllExcept($evil);
21+
22+
var_dump($evil, $storage);
23+
24+
?>
25+
--EXPECTF--
26+
object(EvilStorage)#%d (1) {
27+
["storage":"SplObjectStorage":private]=>
28+
array(0) {
29+
}
30+
}
31+
object(SplObjectStorage)#%d (1) {
32+
["storage":"SplObjectStorage":private]=>
33+
array(0) {
34+
}
35+
}

0 commit comments

Comments
 (0)