diff --git a/Zend/tests/lazy_objects/gh21999-001.phpt b/Zend/tests/lazy_objects/gh21999-001.phpt new file mode 100644 index 000000000000..dd338b2b9f42 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21999-001.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-21999: GC inconsistency with lazy object, var_dump(), and object comparison +--CREDITS-- +kid-lxy +--FILE-- +getProperty('a')->setRawValueWithoutLazyInitialization($obj, new stdClass); + var_dump(!$reflector->isUninitializedLazyObject($obj)); + var_dump($obj); // builds obj->properties +} + +$reflector = new ReflectionClass(C::class); +$obj = $reflector->newLazyGhost(function ($obj) { + $obj->__construct(); // throws: initialization fails +}); +test('Ghost', $obj); +try { + // zend_std_compare() fetches obj->properties. zend_compare_symbol_tables() + // adds obj->properties to GC roots. + $obj > $reflector->newLazyProxy(function () { + return new C(); + }); +} catch (\Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + gc_collect_cycles(); +} + +?> +--EXPECTF-- +bool(false) +lazy ghost object(C)#%d (1) { + ["a"]=> + object(stdClass)#%d (0) { + } +} +ArgumentCountError: 2 arguments are required, 1 given diff --git a/Zend/tests/lazy_objects/gh21999-002.phpt b/Zend/tests/lazy_objects/gh21999-002.phpt new file mode 100644 index 000000000000..8807d2546e47 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21999-002.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-21999: GC inconsistency with lazy object, var_dump(), and object comparison +--FILE-- +getProperty('a')->setRawValueWithoutLazyInitialization($obj, new stdClass); + var_dump(!$reflector->isUninitializedLazyObject($obj)); + var_dump($obj); +} + +$reflector = new ReflectionClass(C::class); +$obj = $reflector->newLazyGhost(function ($obj) { + $obj->__construct(); +}); +$reflector->getProperty('a')->setRawValueWithoutLazyInitialization($obj, $obj); + +try { + var_dump($obj < $reflector->newLazyGhost(function () {})); +} catch (\Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + gc_collect_cycles(); +} + +?> +--EXPECT-- +ArgumentCountError: 2 arguments are required, 1 given diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index 59c8ec36a9b8..2ca3a5e46569 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -797,16 +797,28 @@ HashTable *zend_lazy_object_get_gc(zend_object *zobj, zval **table, int *n) } zend_get_gc_buffer_add_zval(gc_buffer, &info->u.initializer.zv); - /* Uninitialized lazy objects can not have dynamic properties, so we can - * ignore zobj->properties. */ - zval *prop = zobj->properties_table; - zval *end = prop + zobj->ce->default_properties_count; - for ( ; prop < end; prop++) { - zend_get_gc_buffer_add_zval(gc_buffer, prop); + /* Lazy objects may have a properties ht in two cases: + * - After fetching debug infos + * - After lazy init failed in zend_std_get_properties() + * + * In the latter case zobj->properties is an empty ht. We should ignore it, + * otherwise we may mask references to the GC. In the former case we must + * return it, otherwise the GC may traverse properties twice in case the ht + * it a root by itself. */ + zend_array *ht; + if (zobj->properties && zend_hash_num_elements(zobj->properties) > 0) { + ht = zobj->properties; + } else { + zval *prop = zobj->properties_table; + zval *end = prop + zobj->ce->default_properties_count; + for ( ; prop < end; prop++) { + zend_get_gc_buffer_add_zval(gc_buffer, prop); + } + ht = NULL; } zend_get_gc_buffer_use(gc_buffer, table, n); - return NULL; + return ht; } zend_property_info *zend_lazy_object_get_property_info_for_slot(zend_object *obj, zval *slot)