Skip to content

Commit 43a4f91

Browse files
committed
Fix concurrent iteration and deletion issues in SplObjectStorage
Closes GH-21443.
1 parent 51115d4 commit 43a4f91

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
@@ -26,6 +26,8 @@ PHP NEWS
2626
- SPL:
2727
. Fixed bug GH-21499 (RecursiveArrayIterator getChildren UAF after parent
2828
free). (Girgias)
29+
. Fix concurrent iteration and deletion issues in SplObjectStorage.
30+
(ndossche)
2931

3032
- Streams:
3133
. 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
@@ -240,11 +240,25 @@ static zend_result spl_object_storage_detach(spl_SplObjectStorage *intern, zend_
240240
return ret;
241241
} /* }}}*/
242242

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

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

250264
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)