Skip to content

Fix use-after-free during lazy object initialization #16004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions Zend/tests/lazy_objects/gh15999_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
--TEST--
Lazy Objects: GH-15999 001: Object is released during initialization
--FILE--
<?php

class C {
public $s;
public function __destruct() {
var_dump(__METHOD__);
}
}

print "# Ghost:\n";

$r = new ReflectionClass(C::class);

$o = $r->newLazyGhost(function ($obj) {
global $o;
$o = null;
});
$p = new stdClass;

try {
$o->s = $p;
} catch (Error $e) {
printf("%s: %s\n", $e::class, $e->getMessage());
}

print "# Proxy:\n";

$o = $r->newLazyProxy(function ($obj) {
global $o;
$o = null;
return new C();
});
$p = new stdClass;

try {
$o->s = $p;
} catch (Error $e) {
printf("%s: %s\n", $e::class, $e->getMessage());
}

print "# GC cycle:\n";

$o = $r->newLazyGhost(function ($obj) {
global $o;
$o->s = $o;
$o = null;
gc_collect_cycles();
});
$p = new stdClass;

$o->s = $p;
gc_collect_cycles();

print "# Nested error (ghost):\n";

$r = new ReflectionClass(C::class);

$o = $r->newLazyGhost(function ($obj) {
global $o;
$o = null;
return new stdClass;
});
$p = new stdClass;

try {
$o->s = $p;
} catch (Error $e) {
do {
printf("%s: %s\n", $e::class, $e->getMessage());
} while ($e = $e->getPrevious());
}

print "# Nested error (proxy):\n";

$r = new ReflectionClass(C::class);

$o = $r->newLazyProxy(function ($obj) {
global $o;
$o = null;
return new stdClass;
});
$p = new stdClass;

try {
$o->s = $p;
} catch (Error $e) {
do {
printf("%s: %s\n", $e::class, $e->getMessage());
} while ($e = $e->getPrevious());
}

?>
==DONE==
--EXPECT--
# Ghost:
string(13) "C::__destruct"
Error: Lazy object was released during initialization
# Proxy:
string(13) "C::__destruct"
Error: Lazy object was released during initialization
# GC cycle:
string(13) "C::__destruct"
# Nested error (ghost):
Error: Lazy object was released during initialization
TypeError: Lazy object initializer must return NULL or no value
# Nested error (proxy):
Error: Lazy object was released during initialization
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.
==DONE==
55 changes: 55 additions & 0 deletions Zend/tests/lazy_objects/gh15999_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
Lazy Objects: GH-15999 002: Assigned value is changed during lazy object initialization
--FILE--
<?php

class C {
public $s;
public function __destruct() {
var_dump(__METHOD__);
}
}

print "# Ghost:\n";

$r = new ReflectionClass(C::class);

$o = $r->newLazyGhost(function ($obj) {
global $p;
$p = null;
});

$p = new stdClass;
var_dump($o->s = $p);
var_dump($o->s);

print "# Proxy:\n";

$r = new ReflectionClass(C::class);

$o = $r->newLazyProxy(function ($obj) {
global $p;
$p = null;
return new C();
});

$p = new stdClass;
var_dump($o->s = $p);
var_dump($o->s);

?>
==DONE==
--EXPECTF--
# Ghost:
object(stdClass)#%d (0) {
}
object(stdClass)#%d (0) {
}
# Proxy:
string(13) "C::__destruct"
object(stdClass)#%d (0) {
}
object(stdClass)#%d (0) {
}
==DONE==
string(13) "C::__destruct"
47 changes: 38 additions & 9 deletions Zend/zend_lazy_objects.c
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
ZEND_ASSERT(zend_object_is_lazy_proxy(obj));
ZEND_ASSERT(!zend_lazy_object_initialized(obj));

/* Prevent object from being released during initialization */
GC_ADDREF(obj);

zend_lazy_object_info *info = zend_lazy_object_get_info(obj);

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

ZVAL_OBJ(&zobj, obj);

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

if (UNEXPECTED(EG(exception))) {
OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
return NULL;
goto exit;
}

if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT)) {
Expand All @@ -456,8 +460,7 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
ZSTR_VAL(obj->ce->name),
zend_zval_value_name(&retval));
zval_ptr_dtor(&retval);
return NULL;

goto exit;
}

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

if (UNEXPECTED(Z_OBJ(retval) == obj || zend_object_is_lazy(Z_OBJ(retval)))) {
OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
zend_throw_error(NULL, "Lazy proxy factory must return a non-lazy object");
zval_ptr_dtor(&retval);
return NULL;
goto exit;
}

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

return Z_OBJ(retval);
instance = Z_OBJ(retval);

exit:
if (UNEXPECTED(GC_DELREF(obj) == 0)) {
zend_throw_error(NULL, "Lazy object was released during initialization");
zend_objects_store_del(obj);
instance = NULL;
} else {
gc_check_possible_root((zend_refcounted*) obj);
}

return instance;
}

/* Initialize a lazy object. */
Expand Down Expand Up @@ -529,6 +543,9 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
return zend_lazy_object_init_proxy(obj);
}

/* Prevent object from being released during initialization */
GC_ADDREF(obj);

zend_fcall_info_cache *initializer = zend_lazy_object_get_initializer_fcc(obj);

/* Prevent reentrant initialization */
Expand Down Expand Up @@ -562,21 +579,22 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
int argc = 1;
zval zobj;
HashTable *named_params = NULL;
zend_object *instance = NULL;

ZVAL_OBJ(&zobj, obj);

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

if (EG(exception)) {
zend_lazy_object_revert_init(obj, properties_table_snapshot, properties_snapshot);
return NULL;
goto exit;
}

if (Z_TYPE(retval) != IS_NULL) {
zend_lazy_object_revert_init(obj, properties_table_snapshot, properties_snapshot);
zval_ptr_dtor(&retval);
zend_type_error("Lazy object initializer must return NULL or no value");
return NULL;
goto exit;
}

if (properties_table_snapshot) {
Expand All @@ -598,7 +616,18 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj)
* zend_lazy_object_has_stale_info() check */
zend_lazy_object_del_info(obj);

return obj;
instance = obj;

exit:
if (UNEXPECTED(GC_DELREF(obj) == 0)) {
zend_throw_error(NULL, "Lazy object was released during initialization");
zend_objects_store_del(obj);
instance = NULL;
} else {
gc_check_possible_root((zend_refcounted*) obj);
}

return instance;
}

/* Mark an object as non-lazy (after all properties were initialized) */
Expand Down
12 changes: 10 additions & 2 deletions Zend/zend_object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1183,13 +1183,21 @@ found:;
exit:
return variable_ptr;

lazy_init:
lazy_init:;
/* backup value as it may change during initialization */
zval backup;
ZVAL_COPY(&backup, value);

zobj = zend_lazy_object_init(zobj);
if (UNEXPECTED(!zobj)) {
variable_ptr = &EG(error_zval);
zval_ptr_dtor(&backup);
goto exit;
}
return zend_std_write_property(zobj, name, value, cache_slot);

variable_ptr = zend_std_write_property(zobj, name, &backup, cache_slot);
zval_ptr_dtor(&backup);
return variable_ptr;
}
/* }}} */

Expand Down
Loading