Skip to content

Commit dbec591

Browse files
committed
Merge branch 'PHP-8.4'
* PHP-8.4: [ci skip] NEWS for GH-16004 Fix use-after-free during lazy object initialization (#16004)
2 parents dd063b3 + e4335ba commit dbec591

File tree

4 files changed

+215
-11
lines changed

4 files changed

+215
-11
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
--TEST--
2+
Lazy Objects: GH-15999 001: Object is released during initialization
3+
--FILE--
4+
<?php
5+
6+
class C {
7+
public $s;
8+
public function __destruct() {
9+
var_dump(__METHOD__);
10+
}
11+
}
12+
13+
print "# Ghost:\n";
14+
15+
$r = new ReflectionClass(C::class);
16+
17+
$o = $r->newLazyGhost(function ($obj) {
18+
global $o;
19+
$o = null;
20+
});
21+
$p = new stdClass;
22+
23+
try {
24+
$o->s = $p;
25+
} catch (Error $e) {
26+
printf("%s: %s\n", $e::class, $e->getMessage());
27+
}
28+
29+
print "# Proxy:\n";
30+
31+
$o = $r->newLazyProxy(function ($obj) {
32+
global $o;
33+
$o = null;
34+
return new C();
35+
});
36+
$p = new stdClass;
37+
38+
try {
39+
$o->s = $p;
40+
} catch (Error $e) {
41+
printf("%s: %s\n", $e::class, $e->getMessage());
42+
}
43+
44+
print "# GC cycle:\n";
45+
46+
$o = $r->newLazyGhost(function ($obj) {
47+
global $o;
48+
$o->s = $o;
49+
$o = null;
50+
gc_collect_cycles();
51+
});
52+
$p = new stdClass;
53+
54+
$o->s = $p;
55+
gc_collect_cycles();
56+
57+
print "# Nested error (ghost):\n";
58+
59+
$r = new ReflectionClass(C::class);
60+
61+
$o = $r->newLazyGhost(function ($obj) {
62+
global $o;
63+
$o = null;
64+
return new stdClass;
65+
});
66+
$p = new stdClass;
67+
68+
try {
69+
$o->s = $p;
70+
} catch (Error $e) {
71+
do {
72+
printf("%s: %s\n", $e::class, $e->getMessage());
73+
} while ($e = $e->getPrevious());
74+
}
75+
76+
print "# Nested error (proxy):\n";
77+
78+
$r = new ReflectionClass(C::class);
79+
80+
$o = $r->newLazyProxy(function ($obj) {
81+
global $o;
82+
$o = null;
83+
return new stdClass;
84+
});
85+
$p = new stdClass;
86+
87+
try {
88+
$o->s = $p;
89+
} catch (Error $e) {
90+
do {
91+
printf("%s: %s\n", $e::class, $e->getMessage());
92+
} while ($e = $e->getPrevious());
93+
}
94+
95+
?>
96+
==DONE==
97+
--EXPECT--
98+
# Ghost:
99+
string(13) "C::__destruct"
100+
Error: Lazy object was released during initialization
101+
# Proxy:
102+
string(13) "C::__destruct"
103+
Error: Lazy object was released during initialization
104+
# GC cycle:
105+
string(13) "C::__destruct"
106+
# Nested error (ghost):
107+
Error: Lazy object was released during initialization
108+
TypeError: Lazy object initializer must return NULL or no value
109+
# Nested error (proxy):
110+
Error: Lazy object was released during initialization
111+
TypeError: The real instance class stdClass is not compatible with the proxy class C. The proxy must be a instance of the same class as the real instance, or a sub-class with no additional properties, and no overrides of the __destructor or __clone methods.
112+
==DONE==
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
Lazy Objects: GH-15999 002: Assigned value is changed during lazy object initialization
3+
--FILE--
4+
<?php
5+
6+
class C {
7+
public $s;
8+
public function __destruct() {
9+
var_dump(__METHOD__);
10+
}
11+
}
12+
13+
print "# Ghost:\n";
14+
15+
$r = new ReflectionClass(C::class);
16+
17+
$o = $r->newLazyGhost(function ($obj) {
18+
global $p;
19+
$p = null;
20+
});
21+
22+
$p = new stdClass;
23+
var_dump($o->s = $p);
24+
var_dump($o->s);
25+
26+
print "# Proxy:\n";
27+
28+
$r = new ReflectionClass(C::class);
29+
30+
$o = $r->newLazyProxy(function ($obj) {
31+
global $p;
32+
$p = null;
33+
return new C();
34+
});
35+
36+
$p = new stdClass;
37+
var_dump($o->s = $p);
38+
var_dump($o->s);
39+
40+
?>
41+
==DONE==
42+
--EXPECTF--
43+
# Ghost:
44+
object(stdClass)#%d (0) {
45+
}
46+
object(stdClass)#%d (0) {
47+
}
48+
# Proxy:
49+
string(13) "C::__destruct"
50+
object(stdClass)#%d (0) {
51+
}
52+
object(stdClass)#%d (0) {
53+
}
54+
==DONE==
55+
string(13) "C::__destruct"

Zend/zend_lazy_objects.c

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
429429
ZEND_ASSERT(zend_object_is_lazy_proxy(obj));
430430
ZEND_ASSERT(!zend_lazy_object_initialized(obj));
431431

432+
/* Prevent object from being released during initialization */
433+
GC_ADDREF(obj);
434+
432435
zend_lazy_object_info *info = zend_lazy_object_get_info(obj);
433436

434437
/* prevent reentrant initialization */
@@ -440,14 +443,15 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
440443
zval zobj;
441444
HashTable *named_params = NULL;
442445
zend_fcall_info_cache *initializer = &info->u.initializer.fcc;
446+
zend_object *instance = NULL;
443447

444448
ZVAL_OBJ(&zobj, obj);
445449

446450
zend_call_known_fcc(initializer, &retval, argc, &zobj, named_params);
447451

448452
if (UNEXPECTED(EG(exception))) {
449453
OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
450-
return NULL;
454+
goto exit;
451455
}
452456

453457
if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT)) {
@@ -456,8 +460,7 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
456460
ZSTR_VAL(obj->ce->name),
457461
zend_zval_value_name(&retval));
458462
zval_ptr_dtor(&retval);
459-
return NULL;
460-
463+
goto exit;
461464
}
462465

463466
if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || !zend_lazy_object_compatible(Z_OBJ(retval), obj))) {
@@ -466,14 +469,14 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
466469
zend_zval_value_name(&retval),
467470
ZSTR_VAL(obj->ce->name));
468471
zval_ptr_dtor(&retval);
469-
return NULL;
472+
goto exit;
470473
}
471474

472475
if (UNEXPECTED(Z_OBJ(retval) == obj || zend_object_is_lazy(Z_OBJ(retval)))) {
473476
OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
474477
zend_throw_error(NULL, "Lazy proxy factory must return a non-lazy object");
475478
zval_ptr_dtor(&retval);
476-
return NULL;
479+
goto exit;
477480
}
478481

479482
zend_fcc_dtor(&info->u.initializer.fcc);
@@ -495,7 +498,18 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
495498
}
496499
}
497500

498-
return Z_OBJ(retval);
501+
instance = Z_OBJ(retval);
502+
503+
exit:
504+
if (UNEXPECTED(GC_DELREF(obj) == 0)) {
505+
zend_throw_error(NULL, "Lazy object was released during initialization");
506+
zend_objects_store_del(obj);
507+
instance = NULL;
508+
} else {
509+
gc_check_possible_root((zend_refcounted*) obj);
510+
}
511+
512+
return instance;
499513
}
500514

501515
/* Initialize a lazy object. */
@@ -529,6 +543,9 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
529543
return zend_lazy_object_init_proxy(obj);
530544
}
531545

546+
/* Prevent object from being released during initialization */
547+
GC_ADDREF(obj);
548+
532549
zend_fcall_info_cache *initializer = zend_lazy_object_get_initializer_fcc(obj);
533550

534551
/* Prevent reentrant initialization */
@@ -562,21 +579,22 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
562579
int argc = 1;
563580
zval zobj;
564581
HashTable *named_params = NULL;
582+
zend_object *instance = NULL;
565583

566584
ZVAL_OBJ(&zobj, obj);
567585

568586
zend_call_known_fcc(initializer, &retval, argc, &zobj, named_params);
569587

570588
if (EG(exception)) {
571589
zend_lazy_object_revert_init(obj, properties_table_snapshot, properties_snapshot);
572-
return NULL;
590+
goto exit;
573591
}
574592

575593
if (Z_TYPE(retval) != IS_NULL) {
576594
zend_lazy_object_revert_init(obj, properties_table_snapshot, properties_snapshot);
577595
zval_ptr_dtor(&retval);
578596
zend_type_error("Lazy object initializer must return NULL or no value");
579-
return NULL;
597+
goto exit;
580598
}
581599

582600
if (properties_table_snapshot) {
@@ -598,7 +616,18 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
598616
* zend_lazy_object_has_stale_info() check */
599617
zend_lazy_object_del_info(obj);
600618

601-
return obj;
619+
instance = obj;
620+
621+
exit:
622+
if (UNEXPECTED(GC_DELREF(obj) == 0)) {
623+
zend_throw_error(NULL, "Lazy object was released during initialization");
624+
zend_objects_store_del(obj);
625+
instance = NULL;
626+
} else {
627+
gc_check_possible_root((zend_refcounted*) obj);
628+
}
629+
630+
return instance;
602631
}
603632

604633
/* Mark an object as non-lazy (after all properties were initialized) */

Zend/zend_object_handlers.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,13 +1183,21 @@ found:;
11831183
exit:
11841184
return variable_ptr;
11851185

1186-
lazy_init:
1186+
lazy_init:;
1187+
/* backup value as it may change during initialization */
1188+
zval backup;
1189+
ZVAL_COPY(&backup, value);
1190+
11871191
zobj = zend_lazy_object_init(zobj);
11881192
if (UNEXPECTED(!zobj)) {
11891193
variable_ptr = &EG(error_zval);
1194+
zval_ptr_dtor(&backup);
11901195
goto exit;
11911196
}
1192-
return zend_std_write_property(zobj, name, value, cache_slot);
1197+
1198+
variable_ptr = zend_std_write_property(zobj, name, &backup, cache_slot);
1199+
zval_ptr_dtor(&backup);
1200+
return variable_ptr;
11931201
}
11941202
/* }}} */
11951203

0 commit comments

Comments
 (0)