Skip to content

Commit 3ea546a

Browse files
committed
Fix GH-21831: Forbid SplObjectStorage mutation during getHash
1 parent f1d6391 commit 3ea546a

7 files changed

Lines changed: 201 additions & 62 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ PHP NEWS
151151
- SPL:
152152
. DirectoryIterator key can now work better with filesystem supporting larger
153153
directory indexing. (David Carlier)
154+
. Fixed bug GH-21831 (SplObjectStorage::removeAllExcept() use-after-free
155+
with re-entrant getHash()). (Pratik Bhujel)
154156
. Fix bugs GH-8561, GH-8562, GH-8563, and GH-8564 (Fixing various
155157
SplFileObject iterator desync bugs). (iliaal)
156158

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ PHP 8.6 UPGRADE NOTES
7070
logic in their updateTimestamp() method.
7171

7272
- SPL:
73+
. SplObjectStorage::getHash() implementations may no longer mutate any
74+
SplObjectStorage instance. Attempting to do so now throws an Error.
7375
. SplFileObject::next() now advances the stream when no prior current()
7476
call has cached a line. A subsequent current() call returns the new
7577
line rather than the previous one.

ext/spl/spl_observer.c

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ static zend_object_handlers spl_handler_MultipleIterator;
4545
#define SOS_OVERRIDDEN_WRITE_DIMENSION 2
4646
#define SOS_OVERRIDDEN_UNSET_DIMENSION 4
4747

48+
ZEND_TLS uint32_t spl_object_storage_get_hash_depth;
49+
4850
typedef struct _spl_SplObjectStorage { /* {{{ */
4951
HashTable storage;
5052
zend_long index;
@@ -69,6 +71,16 @@ static inline spl_SplObjectStorage *spl_object_storage_from_obj(zend_object *obj
6971

7072
#define Z_SPLOBJSTORAGE_P(zv) spl_object_storage_from_obj(Z_OBJ_P((zv)))
7173

74+
static zend_always_inline bool spl_object_storage_disallow_mutation_during_get_hash(void)
75+
{
76+
if (UNEXPECTED(spl_object_storage_get_hash_depth)) {
77+
zend_throw_error(NULL, "Modification of SplObjectStorage during getHash() is prohibited");
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
7284
static void spl_SplObjectStorage_free_storage(zend_object *object) /* {{{ */
7385
{
7486
spl_SplObjectStorage *intern = spl_object_storage_from_obj(object);
@@ -83,7 +95,10 @@ static zend_result spl_object_storage_get_hash(zend_hash_key *key, spl_SplObject
8395
zval param;
8496
zval rv;
8597
ZVAL_OBJ(&param, obj);
98+
ZVAL_UNDEF(&rv);
99+
spl_object_storage_get_hash_depth++;
86100
zend_call_method_with_1_params(&intern->std, intern->std.ce, &intern->fptr_get_hash, "getHash", &rv, &param);
101+
spl_object_storage_get_hash_depth--;
87102
if (UNEXPECTED(Z_ISUNDEF(rv))) {
88103
/* An exception has occurred */
89104
return FAILURE;
@@ -176,6 +191,10 @@ static spl_SplObjectStorageElement *spl_object_storage_attach_handle(spl_SplObje
176191

177192
static spl_SplObjectStorageElement *spl_object_storage_attach(spl_SplObjectStorage *intern, zend_object *obj, zval *inf) /* {{{ */
178193
{
194+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
195+
return NULL;
196+
}
197+
179198
if (EXPECTED(!(intern->flags & SOS_OVERRIDDEN_WRITE_DIMENSION))) {
180199
return spl_object_storage_attach_handle(intern, obj, inf);
181200
}
@@ -221,6 +240,10 @@ static spl_SplObjectStorageElement *spl_object_storage_attach(spl_SplObjectStora
221240

222241
static zend_result spl_object_storage_detach(spl_SplObjectStorage *intern, zend_object *obj) /* {{{ */
223242
{
243+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
244+
return FAILURE;
245+
}
246+
224247
if (EXPECTED(!(intern->flags & SOS_OVERRIDDEN_UNSET_DIMENSION))) {
225248
return zend_hash_index_del(&intern->storage, obj->handle);
226249
}
@@ -247,20 +270,25 @@ static zend_result spl_object_storage_detach(spl_SplObjectStorage *intern, zend_
247270
if (UNEXPECTED(Z_ISUNDEF_P(_z))) continue; \
248271
_ptr = Z_PTR_P(_z);
249272

250-
static void spl_object_storage_addall(spl_SplObjectStorage *intern, spl_SplObjectStorage *other) { /* {{{ */
273+
static zend_result spl_object_storage_addall(spl_SplObjectStorage *intern, spl_SplObjectStorage *other) { /* {{{ */
251274
spl_SplObjectStorageElement *element;
252275

253276
SPL_SAFE_HASH_FOREACH_PTR(&other->storage, element) {
254277
zval zv;
255278
zend_object *obj = element->obj;
256279
GC_ADDREF(obj);
257280
ZVAL_COPY(&zv, &element->inf);
258-
spl_object_storage_attach(intern, obj, &zv);
281+
if (UNEXPECTED(!spl_object_storage_attach(intern, obj, &zv))) {
282+
zval_ptr_dtor(&zv);
283+
OBJ_RELEASE(obj);
284+
return FAILURE;
285+
}
259286
zval_ptr_dtor(&zv);
260287
OBJ_RELEASE(obj);
261288
} ZEND_HASH_FOREACH_END();
262289

263290
intern->index = 0;
291+
return SUCCESS;
264292
} /* }}} */
265293

266294
#define SPL_OBJECT_STORAGE_CLASS_HAS_OVERRIDE(class_type, zstr_method) \
@@ -446,6 +474,9 @@ PHP_METHOD(SplObjectStorage, attach)
446474
Z_PARAM_ZVAL(inf)
447475
ZEND_PARSE_PARAMETERS_END();
448476
spl_object_storage_attach(intern, obj, inf);
477+
if (UNEXPECTED(EG(exception))) {
478+
RETURN_THROWS();
479+
}
449480
} /* }}} */
450481

451482
// todo: make spl_object_storage_has_dimension return bool as well
@@ -494,6 +525,10 @@ static zval *spl_object_storage_read_dimension(zend_object *object, zval *offset
494525
static void spl_object_storage_write_dimension(zend_object *object, zval *offset, zval *inf)
495526
{
496527
spl_SplObjectStorage *intern = spl_object_storage_from_obj(object);
528+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
529+
return;
530+
}
531+
497532
if (UNEXPECTED(offset == NULL || Z_TYPE_P(offset) != IS_OBJECT || (intern->flags & SOS_OVERRIDDEN_WRITE_DIMENSION))) {
498533
zend_std_write_dimension(object, offset, inf);
499534
return;
@@ -504,6 +539,10 @@ static void spl_object_storage_write_dimension(zend_object *object, zval *offset
504539
static void spl_multiple_iterator_write_dimension(zend_object *object, zval *offset, zval *inf)
505540
{
506541
spl_SplObjectStorage *intern = spl_object_storage_from_obj(object);
542+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
543+
return;
544+
}
545+
507546
if (UNEXPECTED(offset == NULL || Z_TYPE_P(offset) != IS_OBJECT || (intern->flags & SOS_OVERRIDDEN_WRITE_DIMENSION))) {
508547
zend_std_write_dimension(object, offset, inf);
509548
return;
@@ -518,6 +557,10 @@ static void spl_multiple_iterator_write_dimension(zend_object *object, zval *off
518557
static void spl_object_storage_unset_dimension(zend_object *object, zval *offset)
519558
{
520559
spl_SplObjectStorage *intern = spl_object_storage_from_obj(object);
560+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
561+
return;
562+
}
563+
521564
if (UNEXPECTED(Z_TYPE_P(offset) != IS_OBJECT || (intern->flags & SOS_OVERRIDDEN_UNSET_DIMENSION))) {
522565
zend_std_unset_dimension(object, offset);
523566
return;
@@ -535,6 +578,9 @@ PHP_METHOD(SplObjectStorage, detach)
535578
Z_PARAM_OBJ(obj)
536579
ZEND_PARSE_PARAMETERS_END();
537580
spl_object_storage_detach(intern, obj);
581+
if (UNEXPECTED(EG(exception))) {
582+
RETURN_THROWS();
583+
}
538584

539585
zend_hash_internal_pointer_reset_ex(&intern->storage, &intern->pos);
540586
intern->index = 0;
@@ -566,6 +612,9 @@ PHP_METHOD(SplObjectStorage, offsetGet)
566612
ZEND_PARSE_PARAMETERS_END();
567613

568614
if (spl_object_storage_get_hash(&key, intern, obj) == FAILURE) {
615+
if (UNEXPECTED(EG(exception))) {
616+
RETURN_THROWS();
617+
}
569618
RETURN_NULL();
570619
}
571620

@@ -592,7 +641,9 @@ PHP_METHOD(SplObjectStorage, addAll)
592641

593642
other = Z_SPLOBJSTORAGE_P(obj);
594643

595-
spl_object_storage_addall(intern, other);
644+
if (UNEXPECTED(spl_object_storage_addall(intern, other) == FAILURE)) {
645+
RETURN_THROWS();
646+
}
596647

597648
RETURN_LONG(zend_hash_num_elements(&intern->storage));
598649
} /* }}} */
@@ -614,6 +665,9 @@ PHP_METHOD(SplObjectStorage, removeAll)
614665
zend_hash_internal_pointer_reset(&other->storage);
615666
while ((element = zend_hash_get_current_data_ptr(&other->storage)) != NULL) {
616667
if (spl_object_storage_detach(intern, element->obj) == FAILURE) {
668+
if (UNEXPECTED(EG(exception))) {
669+
RETURN_THROWS();
670+
}
617671
zend_hash_move_forward(&other->storage);
618672
}
619673
}
@@ -641,8 +695,19 @@ PHP_METHOD(SplObjectStorage, removeAllExcept)
641695
SPL_SAFE_HASH_FOREACH_PTR(&intern->storage, element) {
642696
zend_object *elem_obj = element->obj;
643697
GC_ADDREF(elem_obj);
644-
if (!spl_object_storage_contains(other, elem_obj)) {
645-
spl_object_storage_detach(intern, elem_obj);
698+
bool contains = spl_object_storage_contains(other, elem_obj);
699+
if (UNEXPECTED(EG(exception))) {
700+
OBJ_RELEASE(elem_obj);
701+
RETURN_THROWS();
702+
}
703+
if (!contains) {
704+
if (spl_object_storage_detach(intern, elem_obj) == FAILURE) {
705+
OBJ_RELEASE(elem_obj);
706+
if (UNEXPECTED(EG(exception))) {
707+
RETURN_THROWS();
708+
}
709+
continue;
710+
}
646711
}
647712
OBJ_RELEASE(elem_obj);
648713
} ZEND_HASH_FOREACH_END();
@@ -663,7 +728,13 @@ PHP_METHOD(SplObjectStorage, contains)
663728
ZEND_PARSE_PARAMETERS_START(1, 1)
664729
Z_PARAM_OBJ(obj)
665730
ZEND_PARSE_PARAMETERS_END();
666-
RETURN_BOOL(spl_object_storage_contains(intern, obj));
731+
732+
bool contains = spl_object_storage_contains(intern, obj);
733+
if (UNEXPECTED(EG(exception))) {
734+
RETURN_THROWS();
735+
}
736+
737+
RETURN_BOOL(contains);
667738
} /* }}} */
668739

669740
/* {{{ Determine number of objects in storage */
@@ -753,6 +824,9 @@ PHP_METHOD(SplObjectStorage, setInfo)
753824
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &inf) == FAILURE) {
754825
RETURN_THROWS();
755826
}
827+
if (UNEXPECTED(spl_object_storage_disallow_mutation_during_get_hash())) {
828+
RETURN_THROWS();
829+
}
756830

757831
if ((element = zend_hash_get_current_data_ptr_ex(&intern->storage, &intern->pos)) == NULL) {
758832
RETURN_NULL();
@@ -947,6 +1021,10 @@ PHP_METHOD(SplObjectStorage, unserialize)
9471021

9481022
if (spl_object_storage_get_hash(&key, intern, Z_OBJ_P(entry)) == FAILURE) {
9491023
zval_ptr_dtor(&inf);
1024+
if (EG(exception)) {
1025+
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
1026+
RETURN_THROWS();
1027+
}
9501028
goto outexcept;
9511029
}
9521030
pelement = spl_object_storage_get(intern, &key);
@@ -960,6 +1038,14 @@ PHP_METHOD(SplObjectStorage, unserialize)
9601038
var_push_dtor(&var_hash, &obj);
9611039
}
9621040
element = spl_object_storage_attach(intern, Z_OBJ_P(entry), Z_ISUNDEF(inf)?NULL:&inf);
1041+
if (UNEXPECTED(!element)) {
1042+
zval_ptr_dtor(&inf);
1043+
if (EG(exception)) {
1044+
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
1045+
RETURN_THROWS();
1046+
}
1047+
goto outexcept;
1048+
}
9631049
var_replace(&var_hash, &inf, &element->inf);
9641050
zval_ptr_dtor(&inf);
9651051
}
@@ -1055,7 +1141,9 @@ PHP_METHOD(SplObjectStorage, __unserialize)
10551141
}
10561142

10571143
ZVAL_DEREF(val);
1058-
spl_object_storage_attach(intern, Z_OBJ_P(key), val);
1144+
if (UNEXPECTED(!spl_object_storage_attach(intern, Z_OBJ_P(key), val))) {
1145+
RETURN_THROWS();
1146+
}
10591147
key = NULL;
10601148
} else {
10611149
key = val;
@@ -1151,8 +1239,14 @@ PHP_METHOD(MultipleIterator, attachIterator)
11511239
}
11521240

11531241
spl_object_storage_attach(intern, iterator, &zinfo);
1242+
if (UNEXPECTED(EG(exception))) {
1243+
RETURN_THROWS();
1244+
}
11541245
} else {
11551246
spl_object_storage_attach(intern, iterator, NULL);
1247+
if (UNEXPECTED(EG(exception))) {
1248+
RETURN_THROWS();
1249+
}
11561250
}
11571251
}
11581252
/* }}} */
@@ -1167,6 +1261,9 @@ PHP_METHOD(MultipleIterator, detachIterator)
11671261
RETURN_THROWS();
11681262
}
11691263
spl_object_storage_detach(intern, Z_OBJ_P(iterator));
1264+
if (UNEXPECTED(EG(exception))) {
1265+
RETURN_THROWS();
1266+
}
11701267

11711268
zend_hash_internal_pointer_reset_ex(&intern->storage, &intern->pos);
11721269
intern->index = 0;
Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,62 @@
11
--TEST--
2-
SplObjectStorage: Concurrent deletion during iteration
2+
SplObjectStorage: Mutation during getHash is prohibited
33
--CREDITS--
44
cnitlrt
55
--FILE--
66
<?php
77
class EvilStorage extends SplObjectStorage {
8+
public bool $mutate = false;
9+
810
public function getHash($obj): string {
911
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-
}
12+
if ($this->mutate) {
13+
$victim[new stdClass()] = null;
1614
}
1715
return spl_object_hash($obj);
1816
}
1917
}
2018

19+
function populate(SplObjectStorage $victim, SplObjectStorage $other): void {
20+
for ($i = 0; $i < 1024; $i++) {
21+
$o = new stdClass();
22+
$victim[$o] = null;
23+
$other[$o] = null;
24+
}
25+
}
26+
2127
$victim = new SplObjectStorage();
2228
$other = new EvilStorage();
2329

24-
for ($i = 0; $i < 1024; $i++) {
25-
$o = new stdClass();
26-
$victim[$o] = null;
27-
$other[$o] = null;
30+
populate($victim, $other);
31+
$other->mutate = true;
32+
33+
try {
34+
$victim->removeAllExcept($other);
35+
} catch (Error $e) {
36+
echo $e->getMessage(), "\n";
2837
}
2938

30-
var_dump($victim->removeAllExcept($other));
39+
var_dump(count($victim), count($other));
3140

3241
unset($victim, $other);
3342
$victim = new SplObjectStorage();
3443
$other = new EvilStorage();
3544

36-
for ($i = 0; $i < 1024; $i++) {
37-
$o = new stdClass();
38-
$victim[$o] = null;
39-
$other[$o] = null;
45+
populate($victim, $other);
46+
$other->mutate = true;
47+
48+
try {
49+
$other->addAll($victim);
50+
} catch (Error $e) {
51+
echo $e->getMessage(), "\n";
4052
}
4153

42-
var_dump($other->addAll($victim));
54+
var_dump(count($victim), count($other));
4355
?>
44-
--EXPECTF--
45-
int(%d)
56+
--EXPECT--
57+
Modification of SplObjectStorage during getHash() is prohibited
58+
int(1024)
59+
int(1024)
60+
Modification of SplObjectStorage during getHash() is prohibited
61+
int(1024)
4662
int(1024)

0 commit comments

Comments
 (0)