Skip to content

Commit d9d3ed1

Browse files
committed
Fix use-after-free during lazy object initialization
1 parent 090b53b commit d9d3ed1

File tree

3 files changed

+149
-9
lines changed

3 files changed

+149
-9
lines changed

Zend/tests/lazy_objects/gh15999.phpt

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
--TEST--
2+
Lazy Objects: GH-15999: 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+
22+
try {
23+
$o->s = $o;
24+
} catch (Error $e) {
25+
printf("%s: %s\n", $e::class, $e->getMessage());
26+
}
27+
28+
print "# Proxy:\n";
29+
30+
$o = $r->newLazyProxy(function ($obj) {
31+
global $o;
32+
$o = null;
33+
return new C();
34+
});
35+
36+
try {
37+
$o->s = $o;
38+
} catch (Error $e) {
39+
printf("%s: %s\n", $e::class, $e->getMessage());
40+
}
41+
42+
print "# GC cycle:\n";
43+
44+
$o = $r->newLazyGhost(function ($obj) {
45+
global $o;
46+
$o->s = $o;
47+
$o = null;
48+
gc_collect_cycles();
49+
});
50+
51+
$o->s = $o;
52+
gc_collect_cycles();
53+
54+
print "# Nested error (ghost):\n";
55+
56+
$r = new ReflectionClass(C::class);
57+
58+
$o = $r->newLazyGhost(function ($obj) {
59+
global $o;
60+
$o = null;
61+
return new stdClass;
62+
});
63+
64+
try {
65+
$o->s = $o;
66+
} catch (Error $e) {
67+
do {
68+
printf("%s: %s\n", $e::class, $e->getMessage());
69+
} while ($e = $e->getPrevious());
70+
}
71+
72+
print "# Nested error (proxy):\n";
73+
74+
$r = new ReflectionClass(C::class);
75+
76+
$o = $r->newLazyProxy(function ($obj) {
77+
global $o;
78+
$o = null;
79+
return new stdClass;
80+
});
81+
82+
try {
83+
$o->s = $o;
84+
} catch (Error $e) {
85+
do {
86+
printf("%s: %s\n", $e::class, $e->getMessage());
87+
} while ($e = $e->getPrevious());
88+
}
89+
90+
?>
91+
==DONE==
92+
--EXPECT--
93+
# Ghost:
94+
string(13) "C::__destruct"
95+
Error: Lazy object was released during initialization
96+
# Proxy:
97+
string(13) "C::__destruct"
98+
Error: Lazy object was released during initialization
99+
# GC cycle:
100+
string(13) "C::__destruct"
101+
# Nested error (ghost):
102+
Error: Lazy object was released during initialization
103+
TypeError: Lazy object initializer must return NULL or no value
104+
# Nested error (proxy):
105+
Error: Lazy object was released during initialization
106+
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.
107+
==DONE==

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,10 @@ found:;
11891189
variable_ptr = &EG(error_zval);
11901190
goto exit;
11911191
}
1192+
/* value may have changed during initialization */
1193+
if (UNEXPECTED(Z_ISREF_P(value))) {
1194+
value = Z_REFVAL_P(value);
1195+
}
11921196
return zend_std_write_property(zobj, name, value, cache_slot);
11931197
}
11941198
/* }}} */

0 commit comments

Comments
 (0)