Skip to content

Commit dd46516

Browse files
committed
Allow readonly properties to be reset once during cloning
1 parent f3d8f09 commit dd46516

File tree

6 files changed

+107
-11
lines changed

6 files changed

+107
-11
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
Readonly property cannot be reset twice during cloning
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar
9+
) {}
10+
11+
public function __clone()
12+
{
13+
$this->bar++;
14+
var_dump($this);
15+
$this->bar++;
16+
}
17+
}
18+
19+
$foo = new Foo(1);
20+
21+
try {
22+
clone $foo;
23+
} catch (Error $exception) {
24+
echo $exception->getMessage() . "\n";
25+
}
26+
27+
?>
28+
--EXPECT--
29+
object(Foo)#2 (1) {
30+
["bar"]=>
31+
int(2)
32+
}
33+
Cannot modify readonly property Foo::$bar
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
Readonly property can be reset once during cloning
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public function __construct(
8+
public readonly int $bar,
9+
public readonly int $baz
10+
) {}
11+
12+
public function __clone()
13+
{
14+
$this->bar++;
15+
}
16+
17+
public function wrongClone()
18+
{
19+
$instance = clone $this;
20+
$instance->baz++;
21+
}
22+
}
23+
24+
$foo = new Foo(1, 1);
25+
$foo2 = clone $foo;
26+
var_dump($foo2);
27+
28+
try {
29+
$foo->wrongClone();
30+
} catch (Error $exception) {
31+
echo $exception->getMessage() . "\n";
32+
}
33+
34+
$foo3 = clone $foo2;
35+
var_dump($foo2);
36+
37+
?>
38+
--EXPECT--
39+
object(Foo)#2 (2) {
40+
["bar"]=>
41+
int(2)
42+
["baz"]=>
43+
int(0)
44+
}
45+
Cannot modify readonly property Foo::$baz
46+
object(Foo)#2 (2) {
47+
["bar"]=>
48+
int(2)
49+
["baz"]=>
50+
int(0)
51+
}

Zend/zend_API.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,7 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen
16931693

16941694
if (class_type->create_object == NULL) {
16951695
zend_object *obj = zend_objects_new(class_type);
1696+
obj->in_clone = 0;
16961697

16971698
ZVAL_OBJ(arg, obj);
16981699
if (properties) {
@@ -1703,6 +1704,7 @@ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zen
17031704
} else {
17041705
ZVAL_OBJ(arg, class_type->create_object(class_type));
17051706
}
1707+
17061708
return SUCCESS;
17071709
}
17081710
/* }}} */

Zend/zend_object_handlers.c

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
634634
}
635635
}
636636
}
637-
if (UNEXPECTED(Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
637+
if (UNEXPECTED(Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT)) {
638638
/* Skip __get() for uninitialized typed properties */
639639
goto uninit_error;
640640
}
@@ -812,10 +812,14 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
812812

813813
if (UNEXPECTED(prop_info)) {
814814
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
815-
Z_TRY_DELREF_P(value);
816-
zend_readonly_property_modification_error(prop_info);
817-
variable_ptr = &EG(error_zval);
818-
goto exit;
815+
if (zobj->in_clone && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINIT)) {
816+
Z_PROP_FLAG_P(variable_ptr) |= IS_PROP_REINIT;
817+
} else {
818+
Z_TRY_DELREF_P(value);
819+
zend_readonly_property_modification_error(prop_info);
820+
variable_ptr = &EG(error_zval);
821+
goto exit;
822+
}
819823
}
820824

821825
ZVAL_COPY_VALUE(&tmp, value);
@@ -832,7 +836,7 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
832836
variable_ptr, value, IS_TMP_VAR, property_uses_strict_types());
833837
goto exit;
834838
}
835-
if (Z_PROP_FLAG_P(variable_ptr) == IS_PROP_UNINIT) {
839+
if (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT) {
836840
/* Writes to uninitialized typed properties bypass __set(). */
837841
goto write_std_property;
838842
}
@@ -1050,7 +1054,7 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
10501054
if (UNEXPECTED(Z_TYPE_P(retval) == IS_UNDEF)) {
10511055
if (EXPECTED(!zobj->ce->__get) ||
10521056
UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET) ||
1053-
UNEXPECTED(prop_info && Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
1057+
UNEXPECTED(prop_info && (Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT))) {
10541058
if (UNEXPECTED(type == BP_VAR_RW || type == BP_VAR_R)) {
10551059
if (UNEXPECTED(prop_info)) {
10561060
zend_throw_error(NULL,
@@ -1145,7 +1149,7 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
11451149
}
11461150
return;
11471151
}
1148-
if (UNEXPECTED(Z_PROP_FLAG_P(slot) == IS_PROP_UNINIT)) {
1152+
if (UNEXPECTED(Z_PROP_FLAG_P(slot) & IS_PROP_UNINIT)) {
11491153
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY)
11501154
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "unset"))) {
11511155
return;
@@ -1760,7 +1764,7 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has
17601764
if (Z_TYPE_P(value) != IS_UNDEF) {
17611765
goto found;
17621766
}
1763-
if (UNEXPECTED(Z_PROP_FLAG_P(value) == IS_PROP_UNINIT)) {
1767+
if (UNEXPECTED(Z_PROP_FLAG_P(value) & IS_PROP_UNINIT)) {
17641768
/* Skip __isset() for uninitialized typed properties */
17651769
result = 0;
17661770
goto exit;

Zend/zend_objects.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ static zend_always_inline void _zend_object_std_init(zend_object *object, zend_c
3333
object->ce = ce;
3434
object->handlers = ce->default_object_handlers;
3535
object->properties = NULL;
36+
object->in_clone = 0;
3637
zend_objects_store_put(object);
3738
if (UNEXPECTED(ce->ce_flags & ZEND_ACC_USE_GUARDS)) {
3839
ZVAL_UNDEF(object->properties_table + object->ce->default_properties_count);
@@ -198,6 +199,7 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
198199
zval *end = src + old_object->ce->default_properties_count;
199200

200201
do {
202+
Z_PROP_FLAG_P(src) &= ~IS_PROP_REINIT;
201203
i_zval_ptr_dtor(dst);
202204
ZVAL_COPY_VALUE_PROP(dst, src);
203205
zval_add_ref(dst);
@@ -255,7 +257,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
255257

256258
if (old_object->ce->clone) {
257259
GC_ADDREF(new_object);
260+
new_object->in_clone = 1;
258261
zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL);
262+
new_object->in_clone = 0;
259263
OBJ_RELEASE(new_object);
260264
}
261265
}

Zend/zend_types.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ struct _zend_object {
503503
const zend_object_handlers *handlers;
504504
HashTable *properties;
505505
zval properties_table[1];
506+
bool in_clone;
506507
};
507508

508509
struct _zend_resource {
@@ -1438,13 +1439,14 @@ static zend_always_inline uint32_t zval_delref_p(zval* pz) {
14381439
* the Z_EXTRA space when copying property default values etc. We define separate
14391440
* macros for this purpose, so this workaround is easier to remove in the future. */
14401441
#define IS_PROP_UNINIT 1
1442+
#define IS_PROP_REINIT 2
14411443
#define Z_PROP_FLAG_P(z) Z_EXTRA_P(z)
14421444
#define ZVAL_COPY_VALUE_PROP(z, v) \
14431445
do { *(z) = *(v); } while (0)
14441446
#define ZVAL_COPY_PROP(z, v) \
1445-
do { ZVAL_COPY(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v); } while (0)
1447+
do { ZVAL_COPY(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v) & IS_PROP_UNINIT; } while (0)
14461448
#define ZVAL_COPY_OR_DUP_PROP(z, v) \
1447-
do { ZVAL_COPY_OR_DUP(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v); } while (0)
1449+
do { ZVAL_COPY_OR_DUP(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v) & IS_PROP_UNINIT; } while (0)
14481450

14491451

14501452
#endif /* ZEND_TYPES_H */

0 commit comments

Comments
 (0)