diff --git a/Zend/tests/readonly_props/readonly_clone.phpt b/Zend/tests/readonly_props/readonly_clone.phpt new file mode 100644 index 0000000000000..05ab6d0ddad77 --- /dev/null +++ b/Zend/tests/readonly_props/readonly_clone.phpt @@ -0,0 +1,74 @@ +--TEST-- +clone can write to readonly properties +--FILE-- +count = ++self::$counter; + $this->foo = 0; + } + + public function count(?int $count = null): static + { + $new = clone $this; + $new->count = $count ?? ++self::$counter; + + return $new; + } + + public function __clone() + { + if (is_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? '', self::class, true)) { + unset($this->count); + } else { + $this->count = ++self::$counter; + } + $this->foo = 1; + } +} + +$a = new Counter(); +var_dump($a); + +var_dump(clone $a); + +$b = $a->count(); +var_dump($b); + +$c = $a->count(123); +var_dump($c); + +?> +--EXPECTF-- +object(Counter)#%d (2) { + ["count"]=> + int(1) + ["foo":"Counter":private]=> + int(0) +} +object(Counter)#%d (2) { + ["count"]=> + int(2) + ["foo":"Counter":private]=> + int(1) +} +object(Counter)#%d (2) { + ["count"]=> + int(3) + ["foo":"Counter":private]=> + int(1) +} +object(Counter)#%d (2) { + ["count"]=> + int(123) + ["foo":"Counter":private]=> + int(1) +} diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 785626adfe7ac..b5bbe40527477 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -965,7 +965,9 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf { zval tmp; - if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) { + if (UNEXPECTED(Z_PROPERTY_GUARD_P(property_val) & IS_PROP_REINIT)) { + Z_PROPERTY_GUARD_P(property_val) &= ~IS_PROP_REINIT; + } else if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) { zend_readonly_property_modification_error(info); return &EG(uninitialized_zval); } @@ -3094,7 +3096,9 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c ZVAL_INDIRECT(result, ptr); zend_property_info *prop_info = CACHED_PTR_EX(cache_slot + 2); if (prop_info) { - if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { + if (UNEXPECTED(Z_PROPERTY_GUARD_P(ptr) & IS_PROP_REINIT)) { + Z_PROPERTY_GUARD_P(ptr) &= ~IS_PROP_REINIT; + } else if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { /* For objects, W/RW/UNSET fetch modes might not actually modify object. * Similar as with magic __get() allow them, but return the value as a copy * to make sure no actual modification is possible. */ diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index d0677f0fe4e96..725e9926fb7f1 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -811,7 +811,9 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva Z_TRY_ADDREF_P(value); if (UNEXPECTED(prop_info)) { - if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { + if (UNEXPECTED(Z_PROPERTY_GUARD_P(variable_ptr) & IS_PROP_REINIT)) { + Z_PROPERTY_GUARD_P(variable_ptr) &= ~IS_PROP_REINIT; + } else if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { Z_TRY_DELREF_P(value); zend_readonly_property_modification_error(prop_info); variable_ptr = &EG(error_zval); @@ -1126,7 +1128,9 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void zval *slot = OBJ_PROP(zobj, property_offset); if (Z_TYPE_P(slot) != IS_UNDEF) { - if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) { + if (UNEXPECTED(Z_PROPERTY_GUARD_P(slot) & IS_PROP_REINIT)) { + Z_PROPERTY_GUARD_P(slot) &= ~IS_PROP_REINIT; + } else if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) { zend_readonly_property_unset_error(prop_info->ce, name); return; } diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index b09ce3b990d5c..06875f9eb879d 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -192,10 +192,13 @@ ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce) ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object) { + zval *src, *dst, *end, *slot; + zend_property_info *prop_info; + if (old_object->ce->default_properties_count) { - zval *src = old_object->properties_table; - zval *dst = new_object->properties_table; - zval *end = src + old_object->ce->default_properties_count; + src = old_object->properties_table; + dst = new_object->properties_table; + end = src + old_object->ce->default_properties_count; do { i_zval_ptr_dtor(dst); @@ -203,10 +206,16 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zval_add_ref(dst); if (UNEXPECTED(Z_ISREF_P(dst)) && (ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(dst)))) { - zend_property_info *prop_info = zend_get_property_info_for_slot(new_object, dst); + prop_info = zend_get_property_info_for_slot(new_object, dst); if (ZEND_TYPE_IS_SET(prop_info->type)) { ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(dst), prop_info); } + } else if (UNEXPECTED(old_object->ce->clone)) { + prop_info = zend_get_property_info_for_slot(new_object, dst); + if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { + slot = OBJ_PROP(new_object, prop_info->offset); + Z_TYPE_INFO_P(slot) |= IN_CLONE; + } } src++; dst++; @@ -256,6 +265,20 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, if (old_object->ce->clone) { GC_ADDREF(new_object); zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL); + if (new_object->ce->default_properties_count) { + dst = new_object->properties_table; + end = dst + new_object->ce->default_properties_count; + do { + prop_info = zend_get_property_info_for_slot(new_object, dst); + if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) { + slot = OBJ_PROP(new_object, prop_info->offset); + if (Z_PROPERTY_GUARD_P(slot) & IN_CLONE) { + Z_PROPERTY_GUARD_P(slot) &= ~IN_CLONE; + } + } + dst++; + } while (dst != end); + } OBJ_RELEASE(new_object); } } diff --git a/Zend/zend_types.h b/Zend/zend_types.h index df64541749d4c..d75db30a8c54b 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -1438,6 +1438,7 @@ static zend_always_inline uint32_t zval_delref_p(zval* pz) { * the Z_EXTRA space when copying property default values etc. We define separate * macros for this purpose, so this workaround is easier to remove in the future. */ #define IS_PROP_UNINIT 1 +#define IS_PROP_REINIT (1<<4) #define Z_PROP_FLAG_P(z) Z_EXTRA_P(z) #define ZVAL_COPY_VALUE_PROP(z, v) \ do { *(z) = *(v); } while (0)