From d924213d48385b45174f130445525aa46ef8d9ab Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 22 Jun 2026 12:10:39 -0400 Subject: [PATCH] Fix use-after-free during lazy object initialization zend_lazy_object_revert_init() and zend_lazy_object_init_proxy() dropped a property value's refcount while the slot or dynamic-properties table still aliased it, so a value whose destructor reaches back through a reference cycle (unset($this->obj->prop)) freed it twice. Clear the slot and detach the properties table before dropping the refcount. Fixes GH-22399 --- Zend/tests/lazy_objects/gh22399.phpt | 39 ++++++++++++++++++ .../lazy_objects/gh22399_dynamic_props.phpt | 40 +++++++++++++++++++ Zend/tests/lazy_objects/gh22399_proxy.phpt | 40 +++++++++++++++++++ Zend/zend_lazy_objects.c | 34 +++++++++++++--- 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/lazy_objects/gh22399.phpt create mode 100644 Zend/tests/lazy_objects/gh22399_dynamic_props.phpt create mode 100644 Zend/tests/lazy_objects/gh22399_proxy.phpt diff --git a/Zend/tests/lazy_objects/gh22399.phpt b/Zend/tests/lazy_objects/gh22399.phpt new file mode 100644 index 000000000000..1e3cde6f9f1f --- /dev/null +++ b/Zend/tests/lazy_objects/gh22399.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-22399 (Lazy object revert init with object destructor can lead to double free) +--FILE-- +a->b); + } +} + +$reflector = new ReflectionClass(A::class); + +$a = $reflector->newLazyGhost(function (A $a) { + $a->b = new B($a); + throw new Exception('initializer exception'); +}); + +try { + $a->any; +} catch (Exception $e) { + printf("%s\n", $e->getMessage()); +} + +var_dump($a); +printf("Is lazy: %d\n", $reflector->isUninitializedLazyObject($a)); +echo "done\n"; +?> +--EXPECTF-- +initializer exception +lazy ghost object(A)#%d (0) { +} +Is lazy: 1 +done diff --git a/Zend/tests/lazy_objects/gh22399_dynamic_props.phpt b/Zend/tests/lazy_objects/gh22399_dynamic_props.phpt new file mode 100644 index 000000000000..ffd5dbe398a4 --- /dev/null +++ b/Zend/tests/lazy_objects/gh22399_dynamic_props.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-22399 (Lazy object revert init double free, dynamic property variant) +--FILE-- +a->dyn); + } +} + +$reflector = new ReflectionClass(A::class); + +$a = $reflector->newLazyGhost(function (A $a) { + $a->dyn = new B($a); + throw new Exception('initializer exception'); +}); + +try { + $a->trigger; +} catch (Exception $e) { + printf("%s\n", $e->getMessage()); +} + +var_dump($a); +printf("Is lazy: %d\n", $reflector->isUninitializedLazyObject($a)); +echo "done\n"; +?> +--EXPECTF-- +initializer exception +lazy ghost object(A)#%d (0) { +} +Is lazy: 1 +done diff --git a/Zend/tests/lazy_objects/gh22399_proxy.phpt b/Zend/tests/lazy_objects/gh22399_proxy.phpt new file mode 100644 index 000000000000..a4d65c7e1076 --- /dev/null +++ b/Zend/tests/lazy_objects/gh22399_proxy.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-22399 (Lazy object double free, proxy cleanup variant) +--FILE-- +proxy->b); + } +} + +class C { + public function __construct(public $proxy) {} + public function __destruct() { + unset($this->proxy->dyn); + } +} + +$reflector = new ReflectionClass(A::class); + +$a = $reflector->newLazyProxy(function (A $a) { + $a->b = new B($a); + $a->dyn = new C($a); + return new A(); +}); + +$a->trigger; +var_dump($reflector->isUninitializedLazyObject($a)); +echo "done\n"; +?> +--EXPECT-- +bool(false) +done diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index 59c8ec36a9b8..5e31e9b298f0 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -417,7 +417,18 @@ static void zend_lazy_object_revert_init(zend_object *obj, zval *properties_tabl } zval *p = &properties_table[OBJ_PROP_TO_NUM(prop_info->offset)]; - zend_object_dtor_property(obj, p); + if (Z_REFCOUNTED_P(p)) { + if (UNEXPECTED(Z_ISREF_P(p)) && + (ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(p)))) { + if (ZEND_TYPE_IS_SET(prop_info->type)) { + ZEND_REF_DEL_TYPE_SOURCE(Z_REF_P(p), prop_info); + } + } + zval garbage; + ZVAL_COPY_VALUE(&garbage, p); + ZVAL_UNDEF(p); + i_zval_ptr_dtor(&garbage); + } ZVAL_COPY_VALUE_PROP(p, &properties_table_snapshot[OBJ_PROP_TO_NUM(prop_info->offset)]); if (Z_ISREF_P(p) && ZEND_TYPE_IS_SET(prop_info->type)) { @@ -430,15 +441,17 @@ static void zend_lazy_object_revert_init(zend_object *obj, zval *properties_tabl if (properties_snapshot) { if (obj->properties != properties_snapshot) { ZEND_ASSERT((GC_FLAGS(properties_snapshot) & IS_ARRAY_IMMUTABLE) || GC_REFCOUNT(properties_snapshot) >= 1); - zend_release_properties(obj->properties); + HashTable *garbage = obj->properties; obj->properties = properties_snapshot; + zend_release_properties(garbage); } else { ZEND_ASSERT((GC_FLAGS(properties_snapshot) & IS_ARRAY_IMMUTABLE) || GC_REFCOUNT(properties_snapshot) > 1); zend_release_properties(properties_snapshot); } } else if (obj->properties) { - zend_release_properties(obj->properties); + HashTable *garbage = obj->properties; obj->properties = NULL; + zend_release_properties(garbage); } OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED; @@ -529,16 +542,27 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj) /* unset() properties of the proxy. This ensures that all accesses are be * delegated to the backing instance from now on. */ - zend_object_dtor_dynamic_properties(obj); + HashTable *garbage_ht = obj->properties; obj->properties = NULL; + zend_release_properties(garbage_ht); for (int i = 0; i < Z_OBJ(retval)->ce->default_properties_count; i++) { zend_property_info *prop_info = Z_OBJ(retval)->ce->properties_info_table[i]; if (EXPECTED(prop_info)) { zval *prop = &obj->properties_table[OBJ_PROP_TO_NUM(prop_info->offset)]; - zend_object_dtor_property(obj, prop); + zval garbage; + ZVAL_COPY_VALUE(&garbage, prop); ZVAL_UNDEF(prop); Z_PROP_FLAG_P(prop) = IS_PROP_UNINIT | IS_PROP_LAZY; + if (Z_REFCOUNTED(garbage)) { + if (UNEXPECTED(Z_ISREF(garbage)) && + (ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF(garbage)))) { + if (ZEND_TYPE_IS_SET(prop_info->type)) { + ZEND_REF_DEL_TYPE_SOURCE(Z_REF(garbage), prop_info); + } + } + i_zval_ptr_dtor(&garbage); + } } }