Skip to content

Commit 7fdd313

Browse files
Allow writing to readonly properties during cloning
1 parent 291c8bd commit 7fdd313

File tree

5 files changed

+115
-8
lines changed

5 files changed

+115
-8
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
--TEST--
2+
clone can write to readonly properties
3+
--FILE--
4+
<?php
5+
6+
class Counter
7+
{
8+
private static int $counter = 0;
9+
10+
public readonly int $count;
11+
private readonly int $foo;
12+
13+
public function __construct()
14+
{
15+
$this->count = ++self::$counter;
16+
$this->foo = 0;
17+
}
18+
19+
public function count(?int $count = null): static
20+
{
21+
$new = clone $this;
22+
$new->count = $count ?? ++self::$counter;
23+
24+
return $new;
25+
}
26+
27+
public function __clone()
28+
{
29+
if (is_a(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['class'] ?? '', self::class, true)) {
30+
unset($this->count);
31+
} else {
32+
$this->count = ++self::$counter;
33+
}
34+
$this->foo = 1;
35+
}
36+
}
37+
38+
$a = new Counter();
39+
var_dump($a);
40+
41+
var_dump(clone $a);
42+
43+
$b = $a->count();
44+
var_dump($b);
45+
46+
$c = $a->count(123);
47+
var_dump($c);
48+
49+
?>
50+
--EXPECT--
51+
object(Counter)#%d (2) {
52+
["count"]=>
53+
int(1)
54+
["foo":"Counter":private]=>
55+
int(0)
56+
}
57+
object(Counter)#%d (2) {
58+
["count"]=>
59+
int(2)
60+
["foo":"Counter":private]=>
61+
int(1)
62+
}
63+
object(Counter)#%d (2) {
64+
["count"]=>
65+
int(3)
66+
["foo":"Counter":private]=>
67+
int(1)
68+
}
69+
object(Counter)#%d (2) {
70+
["count"]=>
71+
int(123)
72+
["foo":"Counter":private]=>
73+
int(1)
74+
}

Zend/zend_execute.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,9 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
965965
{
966966
zval tmp;
967967

968-
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
968+
if (UNEXPECTED(Z_TYPE_EXTRA_P(property_val) == IS_PROP_UNINIT)) {
969+
Z_TYPE_EXTRA_P(property_val) = 0;
970+
} else if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
969971
zend_readonly_property_modification_error(info);
970972
return &EG(uninitialized_zval);
971973
}
@@ -3094,7 +3096,9 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c
30943096
ZVAL_INDIRECT(result, ptr);
30953097
zend_property_info *prop_info = CACHED_PTR_EX(cache_slot + 2);
30963098
if (prop_info) {
3097-
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
3099+
if (UNEXPECTED(Z_TYPE_EXTRA_P(ptr) == IS_PROP_UNINIT)) {
3100+
Z_TYPE_EXTRA_P(ptr) = 0;
3101+
} else if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
30983102
/* For objects, W/RW/UNSET fetch modes might not actually modify object.
30993103
* Similar as with magic __get() allow them, but return the value as a copy
31003104
* to make sure no actual modification is possible. */

Zend/zend_object_handlers.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,9 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
811811
Z_TRY_ADDREF_P(value);
812812

813813
if (UNEXPECTED(prop_info)) {
814-
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
814+
if (UNEXPECTED(Z_TYPE_EXTRA_P(variable_ptr) == IS_PROP_UNINIT)) {
815+
Z_TYPE_EXTRA_P(variable_ptr) = 0;
816+
} else if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
815817
Z_TRY_DELREF_P(value);
816818
zend_readonly_property_modification_error(prop_info);
817819
variable_ptr = &EG(error_zval);
@@ -1126,7 +1128,9 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
11261128
zval *slot = OBJ_PROP(zobj, property_offset);
11271129

11281130
if (Z_TYPE_P(slot) != IS_UNDEF) {
1129-
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) {
1131+
if (UNEXPECTED(Z_TYPE_EXTRA_P(slot) == IS_PROP_UNINIT)) {
1132+
Z_TYPE_EXTRA_P(slot) = 0;
1133+
} else if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) {
11301134
zend_readonly_property_unset_error(prop_info->ce, name);
11311135
return;
11321136
}

Zend/zend_objects.c

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,21 +192,29 @@ ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce)
192192

193193
ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object)
194194
{
195+
zval *src, *dst, *end, *slot;
196+
zend_property_info *prop_info;
197+
195198
if (old_object->ce->default_properties_count) {
196-
zval *src = old_object->properties_table;
197-
zval *dst = new_object->properties_table;
198-
zval *end = src + old_object->ce->default_properties_count;
199+
src = old_object->properties_table;
200+
dst = new_object->properties_table;
201+
end = src + old_object->ce->default_properties_count;
199202

200203
do {
201204
i_zval_ptr_dtor(dst);
202205
ZVAL_COPY_VALUE_PROP(dst, src);
203206
zval_add_ref(dst);
204207
if (UNEXPECTED(Z_ISREF_P(dst)) &&
205208
(ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(dst)))) {
206-
zend_property_info *prop_info = zend_get_property_info_for_slot(new_object, dst);
209+
prop_info = zend_get_property_info_for_slot(new_object, dst);
207210
if (ZEND_TYPE_IS_SET(prop_info->type)) {
208211
ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(dst), prop_info);
209212
}
213+
} else if (UNEXPECTED(old_object->ce->clone)) {
214+
prop_info = zend_get_property_info_for_slot(new_object, dst);
215+
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
216+
Z_TYPE_EXTRA_P(OBJ_PROP(new_object, prop_info->offset)) = IS_PROP_UNINIT;
217+
}
210218
}
211219
src++;
212220
dst++;
@@ -256,6 +264,20 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
256264
if (old_object->ce->clone) {
257265
GC_ADDREF(new_object);
258266
zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL);
267+
if (new_object->ce->default_properties_count) {
268+
dst = new_object->properties_table;
269+
end = dst + new_object->ce->default_properties_count;
270+
do {
271+
prop_info = zend_get_property_info_for_slot(new_object, dst);
272+
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
273+
slot = OBJ_PROP(new_object, prop_info->offset);
274+
if (Z_TYPE_P(slot) != IS_UNDEF && Z_TYPE_EXTRA_P(slot) == IS_PROP_UNINIT) {
275+
Z_TYPE_EXTRA_P(slot) = 0;
276+
}
277+
}
278+
dst++;
279+
} while (dst != end);
280+
}
259281
OBJ_RELEASE(new_object);
260282
}
261283
}

Zend/zend_types.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,9 @@ static zend_always_inline zend_uchar zval_get_type(const zval* pz) {
590590
#define Z_TYPE_INFO(zval) (zval).u1.type_info
591591
#define Z_TYPE_INFO_P(zval_p) Z_TYPE_INFO(*(zval_p))
592592

593+
#define Z_TYPE_EXTRA(zval) (zval).u1.v.u.extra
594+
#define Z_TYPE_EXTRA_P(zval_p) Z_TYPE_EXTRA(*(zval_p))
595+
593596
#define Z_NEXT(zval) (zval).u2.next
594597
#define Z_NEXT_P(zval_p) Z_NEXT(*(zval_p))
595598

0 commit comments

Comments
 (0)