From 4d7fcea5da928266685665ea9f4b50e41da65087 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 3 Oct 2024 14:57:22 +0200 Subject: [PATCH 1/5] Fix handling of undef property during foreach by ref on hooked class --- Zend/tests/property_hooks/foreach.phpt | 2 ++ Zend/zend_property_hooks.c | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Zend/tests/property_hooks/foreach.phpt b/Zend/tests/property_hooks/foreach.phpt index c6073b50ed9..d4b4378a323 100644 --- a/Zend/tests/property_hooks/foreach.phpt +++ b/Zend/tests/property_hooks/foreach.phpt @@ -23,7 +23,9 @@ class ByRef { } } public function __construct() { + $this->undef = 'dynamic'; $this->dynamic = 'dynamic'; + unset($this->undef); } } diff --git a/Zend/zend_property_hooks.c b/Zend/zend_property_hooks.c index 523584de943..6bda610211d 100644 --- a/Zend/zend_property_hooks.c +++ b/Zend/zend_property_hooks.c @@ -169,8 +169,12 @@ static void zho_dynamic_it_fetch_current(zend_object_iterator *iter) HashPosition pos = zend_hash_iterator_pos(hooked_iter->dynamic_prop_it, properties); Bucket *bucket = properties->arData + pos; + + if (UNEXPECTED(Z_TYPE(bucket->val) == IS_UNDEF)) { + return; + } + if (hooked_iter->by_ref && Z_TYPE(bucket->val) != IS_REFERENCE) { - ZEND_ASSERT(Z_TYPE(bucket->val) != IS_UNDEF); ZVAL_MAKE_REF(&bucket->val); } ZVAL_COPY(&hooked_iter->current_data, &bucket->val); From 52fec6958c8c6bd4091008fc5ffb19b258a89b27 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 3 Oct 2024 14:58:57 +0200 Subject: [PATCH 2/5] Do not null out obj->properties when resetting object Engine expects the properties ht to be separated, assigned a new ht, or resized, but never to be nulled. --- Zend/zend_lazy_objects.c | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index e59cde50909..f27f6d2a834 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -199,6 +199,15 @@ ZEND_API bool zend_class_can_be_lazy(zend_class_entry *ce) return true; } +static int zlo_hash_remove_dyn_props_func(zval *pDest) +{ + if (Z_TYPE_P(pDest) == IS_INDIRECT) { + return ZEND_HASH_APPLY_STOP; + } + + return ZEND_HASH_APPLY_REMOVE; +} + /* Make object 'obj' lazy. If 'obj' is NULL, create a lazy instance of * class 'reflection_ce' */ ZEND_API zend_object *zend_object_make_lazy(zend_object *obj, @@ -278,9 +287,17 @@ ZEND_API zend_object *zend_object_make_lazy(zend_object *obj, GC_DEL_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); - /* unset() dynamic properties */ - zend_object_dtor_dynamic_properties(obj); - obj->properties = NULL; + /* unset() dynamic properties. Do not NULL out obj->properties, as this + * would be unexpected. */ + if (obj->properties) { + if (UNEXPECTED(GC_REFCOUNT(obj->properties) > 1)) { + if (EXPECTED(!(GC_FLAGS(obj->properties) & IS_ARRAY_IMMUTABLE))) { + GC_DELREF(obj->properties); + } + obj->properties = zend_array_dup(obj->properties); + } + zend_hash_reverse_apply(obj->properties, zlo_hash_remove_dyn_props_func); + } /* unset() declared properties */ for (int i = 0; i < reflection_ce->default_properties_count; i++) { From 31511179872d35e3d7328df79438620b1b703d2a Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 3 Oct 2024 15:03:26 +0200 Subject: [PATCH 3/5] Ensure to initialize lazy object in foreach foreach() by-passes the get_properties() handler and did not always trigger initialization. --- Zend/zend_vm_def.h | 17 +++++++++++ Zend/zend_vm_execute.h | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 55675e4a0b5..7263c6eef6d 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6821,6 +6821,14 @@ ZEND_VM_HANDLER(77, ZEND_FE_RESET_R, CONST|TMP|VAR|CV, JMP_ADDR) } else if (OP1_TYPE != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { zend_object *zobj = Z_OBJ_P(array_ptr); if (!zobj->ce->get_iterator) { + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + FREE_OP1_IF_VAR(); + HANDLE_EXCEPTION(); + } + } HashTable *properties = zobj->properties; if (properties) { if (UNEXPECTED(GC_REFCOUNT(properties) > 1)) { @@ -6909,7 +6917,16 @@ ZEND_VM_COLD_CONST_HANDLER(125, ZEND_FE_RESET_RW, CONST|TMP|VAR|CV, JMP_ADDR) ZEND_VM_NEXT_OPCODE(); } else if (OP1_TYPE != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { if (!Z_OBJCE_P(array_ptr)->get_iterator) { + zend_object *zobj = Z_OBJ_P(array_ptr); HashTable *properties; + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + FREE_OP1_IF_VAR(); + HANDLE_EXCEPTION(); + } + } if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) { if (array_ptr == array_ref) { ZVAL_NEW_REF(array_ref, array_ref); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 088c29cf408..f0ecc76bbbc 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5411,6 +5411,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_CONST_HANDLER( } else if (IS_CONST != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { zend_object *zobj = Z_OBJ_P(array_ptr); if (!zobj->ce->get_iterator) { + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } HashTable *properties = zobj->properties; if (properties) { if (UNEXPECTED(GC_REFCOUNT(properties) > 1)) { @@ -5497,7 +5505,16 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_RW_SPEC_ ZEND_VM_NEXT_OPCODE(); } else if (IS_CONST != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { if (!Z_OBJCE_P(array_ptr)->get_iterator) { + zend_object *zobj = Z_OBJ_P(array_ptr); HashTable *properties; + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } if (IS_CONST == IS_VAR || IS_CONST == IS_CV) { if (array_ptr == array_ref) { ZVAL_NEW_REF(array_ref, array_ref); @@ -20117,6 +20134,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_TMP_HANDLER(ZE } else if (IS_TMP_VAR != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { zend_object *zobj = Z_OBJ_P(array_ptr); if (!zobj->ce->get_iterator) { + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } HashTable *properties = zobj->properties; if (properties) { if (UNEXPECTED(GC_REFCOUNT(properties) > 1)) { @@ -20204,7 +20229,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_RW_SPEC_TMP_HANDLER(Z ZEND_VM_NEXT_OPCODE(); } else if (IS_TMP_VAR != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { if (!Z_OBJCE_P(array_ptr)->get_iterator) { + zend_object *zobj = Z_OBJ_P(array_ptr); HashTable *properties; + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } if (IS_TMP_VAR == IS_VAR || IS_TMP_VAR == IS_CV) { if (array_ptr == array_ref) { ZVAL_NEW_REF(array_ref, array_ref); @@ -22769,6 +22803,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_VAR_HANDLER(ZE } else if (IS_VAR != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { zend_object *zobj = Z_OBJ_P(array_ptr); if (!zobj->ce->get_iterator) { + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + HANDLE_EXCEPTION(); + } + } HashTable *properties = zobj->properties; if (properties) { if (UNEXPECTED(GC_REFCOUNT(properties) > 1)) { @@ -22857,7 +22899,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_RW_SPEC_VAR_HANDLER(Z ZEND_VM_NEXT_OPCODE(); } else if (IS_VAR != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { if (!Z_OBJCE_P(array_ptr)->get_iterator) { + zend_object *zobj = Z_OBJ_P(array_ptr); HashTable *properties; + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + HANDLE_EXCEPTION(); + } + } if (IS_VAR == IS_VAR || IS_VAR == IS_CV) { if (array_ptr == array_ref) { ZVAL_NEW_REF(array_ref, array_ref); @@ -41067,6 +41118,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_R_SPEC_CV_HANDLER(ZEN } else if (IS_CV != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { zend_object *zobj = Z_OBJ_P(array_ptr); if (!zobj->ce->get_iterator) { + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } HashTable *properties = zobj->properties; if (properties) { if (UNEXPECTED(GC_REFCOUNT(properties) > 1)) { @@ -41153,7 +41212,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FE_RESET_RW_SPEC_CV_HANDLER(ZE ZEND_VM_NEXT_OPCODE(); } else if (IS_CV != IS_CONST && EXPECTED(Z_TYPE_P(array_ptr) == IS_OBJECT)) { if (!Z_OBJCE_P(array_ptr)->get_iterator) { + zend_object *zobj = Z_OBJ_P(array_ptr); HashTable *properties; + if (UNEXPECTED(zend_object_is_lazy(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(EG(exception))) { + UNDEF_RESULT(); + + HANDLE_EXCEPTION(); + } + } if (IS_CV == IS_VAR || IS_CV == IS_CV) { if (array_ptr == array_ref) { ZVAL_NEW_REF(array_ref, array_ref); From c9dfb77446e3c3c9ef3f88c5dd82f42844881828 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 3 Oct 2024 15:05:08 +0200 Subject: [PATCH 4/5] Deny resetting an object as lazy during property iteration Supporting object reset while its properties are being iterated would increase complexity for little benefit. Furthermore it may not be possible to ensure a consistent behavior between ghosts and proxies (wrt to iteration position). Iteration is detected by checking if the object's properties ht has iterators. This requires refactoring the hooked get_iterator() implementation to ensure that it creates a properties ht iterator immediately. Closes GH-15960 --- .../lazy_objects/init_trigger_foreach.phpt | 111 ++++++++++++++- .../init_trigger_foreach_hooks.phpt | 129 ++++++++++++++---- .../reset_as_lazy_resets_dynamic_props.phpt | 4 +- Zend/zend_lazy_objects.c | 19 +++ Zend/zend_property_hooks.c | 67 +++++---- 5 files changed, 266 insertions(+), 64 deletions(-) diff --git a/Zend/tests/lazy_objects/init_trigger_foreach.phpt b/Zend/tests/lazy_objects/init_trigger_foreach.phpt index b8bd780666b..fda50b84c73 100644 --- a/Zend/tests/lazy_objects/init_trigger_foreach.phpt +++ b/Zend/tests/lazy_objects/init_trigger_foreach.phpt @@ -5,9 +5,11 @@ Lazy objects: Foreach initializes object class C { public int $a; + public int $b; public function __construct() { var_dump(__METHOD__); $this->a = 1; + $this->b = 2; } } @@ -24,6 +26,17 @@ foreach ($obj as $prop => $value) { var_dump($prop, $value); } +print "# Ghost (by ref):\n"; + +$obj = $reflector->newLazyGhost(function ($obj) { + var_dump("initializer"); + $obj->__construct(); +}); + +foreach ($obj as $prop => &$value) { + var_dump($prop, $value); +} + print "# Proxy:\n"; $obj = $reflector->newLazyProxy(function ($obj) { @@ -35,14 +48,110 @@ foreach ($obj as $prop => $value) { var_dump($prop, $value); } ---EXPECTF-- +print "# Proxy (by ref):\n"; + +$obj = $reflector->newLazyProxy(function ($obj) { + var_dump("initializer"); + return new C(); +}); + +foreach ($obj as $prop => &$value) { + var_dump($prop, $value); +} + +print "# Ghost (init failure)\n"; + +$fail = true; +$obj = $reflector->newLazyGhost(function ($obj) use (&$fail) { + if ($fail) { + throw new Exception("initializer"); + } else { + var_dump("initializer"); + $obj->__construct(); + } +}); + +try { + foreach ($obj as $prop => $value) { + var_dump($prop, $value); + } +} catch (Exception $e) { + printf("%s: %s\n", $e::class, $e->getMessage()); +} + +$fail = false; +foreach ($obj as $prop => $value) { + var_dump($prop, $value); +} + +print "# Ghost (init failure, by ref)\n"; + +$fail = true; +$obj = $reflector->newLazyGhost(function ($obj) use (&$fail) { + if ($fail) { + throw new Exception("initializer"); + } else { + var_dump("initializer"); + $obj->__construct(); + } +}); + +try { + foreach ($obj as $prop => &$value) { + var_dump($prop, $value); + } +} catch (Exception $e) { + printf("%s: %s\n", $e::class, $e->getMessage()); +} + +$fail = false; +foreach ($obj as $prop => &$value) { + var_dump($prop, $value); +} + +?> +--EXPECT-- # Ghost: string(11) "initializer" string(14) "C::__construct" string(1) "a" int(1) +string(1) "b" +int(2) +# Ghost (by ref): +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) # Proxy: string(11) "initializer" string(14) "C::__construct" string(1) "a" int(1) +string(1) "b" +int(2) +# Proxy (by ref): +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) +# Ghost (init failure) +Exception: initializer +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) +# Ghost (init failure, by ref) +Exception: initializer +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) diff --git a/Zend/tests/lazy_objects/init_trigger_foreach_hooks.phpt b/Zend/tests/lazy_objects/init_trigger_foreach_hooks.phpt index d955c92c5ab..27ac8bf7228 100644 --- a/Zend/tests/lazy_objects/init_trigger_foreach_hooks.phpt +++ b/Zend/tests/lazy_objects/init_trigger_foreach_hooks.phpt @@ -6,49 +6,79 @@ Lazy objects: Foreach initializes object #[AllowDynamicProperties] class C { public int $a; + private int $_b; public int $b { - get { return $this->b; } - set(int $value) { $this->b = $value; } + &get { $ref = &$this->_b; return $ref; } } - public int $c { - get { return $this->a + 2; } - } - public function __construct() { + public function __construct(bool $addDynamic = true) { var_dump(__METHOD__); $this->a = 1; - $this->b = 2; - $this->d = 4; + $this->_b = 2; + if ($addDynamic) { + $this->c = 3; + $this->d = 4; + unset($this->c); + } } } $reflector = new ReflectionClass(C::class); -print "# Ghost:\n"; +function test(string $name, object $obj) { + printf("# %s:\n", $name); + foreach ($obj as $prop => $value) { + var_dump($prop, $value); + } + foreach ($obj as $prop => &$value) { + var_dump($prop, $value); + } +} $obj = $reflector->newLazyGhost(function ($obj) { var_dump("initializer"); $obj->__construct(); }); -foreach ($obj as $prop => $value) { - var_dump($prop, $value); -} - -print "# Proxy:\n"; +test('Ghost', $obj); $obj = $reflector->newLazyProxy(function ($obj) { var_dump("initializer"); return new C(); }); -foreach ($obj as $prop => $value) { - var_dump($prop, $value); -} +test('Proxy', $obj); + +$obj = $reflector->newLazyGhost(function ($obj) { + var_dump("initializer"); + $obj->__construct(addDynamic: false); +}); + +test('Ghost (no dynamic)', $obj); + +$obj = $reflector->newLazyProxy(function ($obj) { + var_dump("initializer"); + return new C(addDynamic: false); +}); + +test('Proxy (no dynamic)', $obj); + +print "# Proxy of proxy (initialization)\n"; + +$obj = $reflector->newLazyProxy(function ($obj) use (&$obj2, $reflector) { + var_dump("initializer"); + return $obj2 = new C(); +}); +$reflector->initializeLazyObject($obj); +$reflector->resetAsLazyProxy($obj2, function () { + return new C(); +}); + +test('Proxy of proxy', $obj); print "# Ghost (init exception):\n"; $obj = $reflector->newLazyGhost(function ($obj) { - throw new \Exception(); + throw new \Exception("initializer"); }); try { @@ -60,7 +90,7 @@ try { print "# Proxy (init exception):\n"; $obj = $reflector->newLazyProxy(function ($obj) { - throw new \Exception(); + throw new \Exception("initializer"); }); try { @@ -77,8 +107,12 @@ string(1) "a" int(1) string(1) "b" int(2) -string(1) "c" -int(3) +string(1) "d" +int(4) +string(1) "a" +int(1) +string(1) "b" +int(2) string(1) "d" int(4) # Proxy: @@ -88,9 +122,54 @@ string(1) "a" int(1) string(1) "b" int(2) -string(1) "c" -int(3) +string(1) "d" +int(4) +string(1) "a" +int(1) +string(1) "b" +int(2) +string(1) "d" +int(4) +# Ghost (no dynamic): +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) +string(1) "a" +int(1) +string(1) "b" +int(2) +# Proxy (no dynamic): +string(11) "initializer" +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) +string(1) "a" +int(1) +string(1) "b" +int(2) +# Proxy of proxy (initialization) +string(11) "initializer" +string(14) "C::__construct" +# Proxy of proxy: +string(14) "C::__construct" +string(1) "a" +int(1) +string(1) "b" +int(2) +string(1) "d" +int(4) +string(1) "a" +int(1) +string(1) "b" +int(2) +string(1) "d" +int(4) # Ghost (init exception): -Exception: +Exception: initializer # Proxy (init exception): -Exception: +Exception: initializer diff --git a/Zend/tests/lazy_objects/reset_as_lazy_resets_dynamic_props.phpt b/Zend/tests/lazy_objects/reset_as_lazy_resets_dynamic_props.phpt index d99a30b3c11..9de565a929a 100644 --- a/Zend/tests/lazy_objects/reset_as_lazy_resets_dynamic_props.phpt +++ b/Zend/tests/lazy_objects/reset_as_lazy_resets_dynamic_props.phpt @@ -47,7 +47,7 @@ var_dump($obj); --EXPECTF-- # Ghost: string(18) "Canary::__destruct" -lazy ghost object(C)#%d (0) { +lazy ghost object(C)#%d (%d) { } string(11) "initializer" object(Canary)#%d (0) { @@ -62,7 +62,7 @@ object(C)#%d (2) { # Proxy: string(18) "Canary::__destruct" string(18) "Canary::__destruct" -lazy proxy object(C)#%d (0) { +lazy proxy object(C)#%d (%d) { } string(11) "initializer" object(Canary)#%d (0) { diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index f27f6d2a834..94be396f580 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -208,6 +208,18 @@ static int zlo_hash_remove_dyn_props_func(zval *pDest) return ZEND_HASH_APPLY_REMOVE; } +static bool zlo_is_iterating(zend_object *object) +{ + if (object->properties && HT_ITERATORS_COUNT(object->properties)) { + return true; + } + if (zend_object_is_lazy_proxy(object) + && zend_lazy_object_initialized(object)) { + return zlo_is_iterating(zend_lazy_object_get_instance(object)); + } + return false; +} + /* Make object 'obj' lazy. If 'obj' is NULL, create a lazy instance of * class 'reflection_ce' */ ZEND_API zend_object *zend_object_make_lazy(zend_object *obj, @@ -260,6 +272,10 @@ ZEND_API zend_object *zend_object_make_lazy(zend_object *obj, } } } else { + if (zlo_is_iterating(obj)) { + zend_throw_error(NULL, "Can not reset an object during property iteration"); + return NULL; + } if (zend_object_is_lazy(obj)) { ZEND_ASSERT(zend_object_is_lazy_proxy(obj) && zend_lazy_object_initialized(obj)); OBJ_EXTRA_FLAGS(obj) &= ~(IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY); @@ -544,6 +560,9 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj) ZEND_ASSERT(zend_object_is_lazy_proxy(obj)); zend_lazy_object_info *info = zend_lazy_object_get_info(obj); ZEND_ASSERT(info->flags & ZEND_LAZY_OBJECT_INITIALIZED); + if (zend_object_is_lazy(info->u.instance)) { + return zend_lazy_object_init(info->u.instance); + } return info->u.instance; } diff --git a/Zend/zend_property_hooks.c b/Zend/zend_property_hooks.c index 6bda610211d..398bf2e559e 100644 --- a/Zend/zend_property_hooks.c +++ b/Zend/zend_property_hooks.c @@ -51,13 +51,6 @@ static uint32_t zho_num_backed_props(zend_object *zobj) static zend_array *zho_build_properties_ex(zend_object *zobj, bool check_access, bool include_dynamic_props) { - if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { - zobj = zend_lazy_object_init(zobj); - if (UNEXPECTED(!zobj)) { - return zend_new_array(0); - } - } - zend_class_entry *ce = zobj->ce; zend_array *properties = zend_new_array(ce->default_properties_count); zend_hash_real_init_mixed(properties); @@ -93,23 +86,22 @@ static zend_array *zho_build_properties_ex(zend_object *zobj, bool check_access, ZEND_API zend_array *zend_hooked_object_build_properties(zend_object *zobj) { + if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(!zobj)) { + return (zend_array*) &zend_empty_array; + } + } + return zho_build_properties_ex(zobj, false, true); } -static bool zho_dynamic_it_init(zend_hooked_object_iterator *hooked_iter) +static void zho_dynamic_it_init(zend_hooked_object_iterator *hooked_iter) { - if (hooked_iter->dynamic_prop_it != (uint32_t) -1) { - return true; - } - zend_object *zobj = Z_OBJ_P(&hooked_iter->it.data); - if (!zobj->properties || zho_num_backed_props(zobj) == zobj->properties->nNumUsed) { - hooked_iter->dynamic_props_done = true; - return false; - } - - hooked_iter->dynamic_prop_it = zend_hash_iterator_add(zobj->properties, zho_num_backed_props(zobj)); - return true; + zend_array *properties = zobj->handlers->get_properties(zobj); + hooked_iter->dynamic_props_done = false; + hooked_iter->dynamic_prop_it = zend_hash_iterator_add(properties, zho_num_backed_props(zobj)); } static void zho_it_get_current_key(zend_object_iterator *iter, zval *key); @@ -163,11 +155,14 @@ static void zho_declared_it_fetch_current(zend_object_iterator *iter) static void zho_dynamic_it_fetch_current(zend_object_iterator *iter) { zend_hooked_object_iterator *hooked_iter = (zend_hooked_object_iterator*)iter; - ZEND_ASSERT(hooked_iter->dynamic_prop_it != (uint32_t) -1); - zend_array *properties = Z_OBJ(iter->data)->properties; HashPosition pos = zend_hash_iterator_pos(hooked_iter->dynamic_prop_it, properties); + if (pos >= properties->nNumUsed) { + hooked_iter->dynamic_props_done = true; + return; + } + Bucket *bucket = properties->arData + pos; if (UNEXPECTED(Z_TYPE(bucket->val) == IS_UNDEF)) { @@ -196,7 +191,7 @@ static void zho_it_fetch_current(zend_object_iterator *iter) while (true) { if (!hooked_iter->declared_props_done) { zho_declared_it_fetch_current(iter); - } else if (!hooked_iter->dynamic_props_done && zho_dynamic_it_init(hooked_iter)) { + } else if (!hooked_iter->dynamic_props_done) { zho_dynamic_it_fetch_current(iter); } else { break; @@ -215,9 +210,7 @@ static void zho_it_dtor(zend_object_iterator *iter) zval_ptr_dtor(&hooked_iter->declared_props); zval_ptr_dtor_nogc(&hooked_iter->current_key); zval_ptr_dtor(&hooked_iter->current_data); - if (hooked_iter->dynamic_prop_it != (uint32_t) -1) { - zend_hash_iterator_del(hooked_iter->dynamic_prop_it); - } + zend_hash_iterator_del(hooked_iter->dynamic_prop_it); } static zend_result zho_it_valid(zend_object_iterator *iter) @@ -256,14 +249,11 @@ static void zho_it_move_forward(zend_object_iterator *iter) if (zend_hash_has_more_elements(properties) != SUCCESS) { hooked_iter->declared_props_done = true; } - } else if (!hooked_iter->dynamic_props_done && zho_dynamic_it_init(hooked_iter)) { + } else if (!hooked_iter->dynamic_props_done) { zend_array *properties = Z_OBJ(iter->data)->properties; HashPosition pos = zend_hash_iterator_pos(hooked_iter->dynamic_prop_it, properties); pos++; EG(ht_iterators)[hooked_iter->dynamic_prop_it].pos = pos; - if (pos >= properties->nNumUsed) { - hooked_iter->dynamic_props_done = true; - } } } @@ -280,9 +270,7 @@ static void zho_it_rewind(zend_object_iterator *iter) zend_array *properties = Z_ARR(hooked_iter->declared_props); zend_hash_internal_pointer_reset(properties); hooked_iter->dynamic_props_done = false; - if (hooked_iter->dynamic_prop_it != (uint32_t) -1) { - EG(ht_iterators)[hooked_iter->dynamic_prop_it].pos = zho_num_backed_props(Z_OBJ(iter->data)); - } + EG(ht_iterators)[hooked_iter->dynamic_prop_it].pos = zho_num_backed_props(Z_OBJ(iter->data)); } static HashTable *zho_it_get_gc(zend_object_iterator *iter, zval **table, int *n) @@ -309,17 +297,24 @@ static const zend_object_iterator_funcs zend_hooked_object_it_funcs = { ZEND_API zend_object_iterator *zend_hooked_object_get_iterator(zend_class_entry *ce, zval *object, int by_ref) { + zend_object *zobj = Z_OBJ_P(object); + if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { + zobj = zend_lazy_object_init(zobj); + if (UNEXPECTED(!zobj)) { + return NULL; + } + } + zend_hooked_object_iterator *iterator = emalloc(sizeof(zend_hooked_object_iterator)); zend_iterator_init(&iterator->it); - ZVAL_OBJ_COPY(&iterator->it.data, Z_OBJ_P(object)); + ZVAL_OBJ_COPY(&iterator->it.data, zobj); iterator->it.funcs = &zend_hooked_object_it_funcs; iterator->by_ref = by_ref; iterator->declared_props_done = false; - zend_array *properties = zho_build_properties_ex(Z_OBJ_P(object), true, false); + zend_array *properties = zho_build_properties_ex(zobj, true, false); ZVAL_ARR(&iterator->declared_props, properties); - iterator->dynamic_props_done = false; - iterator->dynamic_prop_it = (uint32_t) -1; + zho_dynamic_it_init(iterator); ZVAL_UNDEF(&iterator->current_key); ZVAL_UNDEF(&iterator->current_data); From e02e6be633a44a24e9213b9a0a08ae9afa545a55 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 3 Oct 2024 15:13:42 +0200 Subject: [PATCH 5/5] [ci skip] NEWS for GH-15960 --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index b2321d0452b..061aed24c23 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,7 @@ PHP NEWS . Fixed bug GH-15999 (zend_std_write_property() assertion failure with lazy objects). (Arnaud) . Fixed bug GH-15866 (Core dumped in Zend/zend_generators.c). (Arnaud) + . Fixed bug GH-15960 (Foreach edge cases with lazy objects). (Arnaud) - DOM: . Fixed bug GH-16039 (Segmentation fault (access null pointer) in