Skip to content

Commit 5784f95

Browse files
Allow writing to readonly properties during cloning
1 parent 8d78dce commit 5784f95

File tree

5 files changed

+114
-8
lines changed

5 files changed

+114
-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+
--EXPECTF--
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_PROPERTY_GUARD_P(property_val) & IS_PROP_REINIT)) {
969+
Z_PROPERTY_GUARD_P(property_val) &= ~IS_PROP_REINIT;
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_PROPERTY_GUARD_P(ptr) & IS_PROP_REINIT)) {
3100+
Z_PROPERTY_GUARD_P(ptr) &= ~IS_PROP_REINIT;
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_PROPERTY_GUARD_P(variable_ptr) & IS_PROP_REINIT)) {
815+
Z_PROPERTY_GUARD_P(variable_ptr) &= ~IS_PROP_REINIT;
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_PROPERTY_GUARD_P(slot) & IS_PROP_REINIT)) {
1132+
Z_PROPERTY_GUARD_P(slot) &= ~IS_PROP_REINIT;
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: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,21 +192,30 @@ 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+
slot = OBJ_PROP(new_object, prop_info->offset);
217+
Z_TYPE_INFO_P(slot) |= IN_CLONE;
218+
}
210219
}
211220
src++;
212221
dst++;
@@ -256,6 +265,20 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
256265
if (old_object->ce->clone) {
257266
GC_ADDREF(new_object);
258267
zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL);
268+
if (new_object->ce->default_properties_count) {
269+
dst = new_object->properties_table;
270+
end = dst + new_object->ce->default_properties_count;
271+
do {
272+
prop_info = zend_get_property_info_for_slot(new_object, dst);
273+
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
274+
slot = OBJ_PROP(new_object, prop_info->offset);
275+
if (Z_PROPERTY_GUARD_P(slot) & IN_CLONE) {
276+
Z_PROPERTY_GUARD_P(slot) &= ~IN_CLONE;
277+
}
278+
}
279+
dst++;
280+
} while (dst != end);
281+
}
259282
OBJ_RELEASE(new_object);
260283
}
261284
}

Zend/zend_types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@ static zend_always_inline uint32_t zval_delref_p(zval* pz) {
14381438
* the Z_EXTRA space when copying property default values etc. We define separate
14391439
* macros for this purpose, so this workaround is easier to remove in the future. */
14401440
#define IS_PROP_UNINIT 1
1441+
#define IS_PROP_REINIT (1<<4)
14411442
#define Z_PROP_FLAG_P(z) Z_EXTRA_P(z)
14421443
#define ZVAL_COPY_VALUE_PROP(z, v) \
14431444
do { *(z) = *(v); } while (0)

0 commit comments

Comments
 (0)