Skip to content

Commit 01f03db

Browse files
committed
Allow readonly properties to be reinitialized once during cloning
1 parent db6840b commit 01f03db

10 files changed

+291
-17
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 = 2;
14+
var_dump($this);
15+
$this->bar = 3;
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+
echo "done";
28+
29+
?>
30+
--EXPECT--
31+
object(Foo)#2 (1) {
32+
["bar"]=>
33+
int(2)
34+
}
35+
Cannot modify readonly property Foo::$bar
36+
done
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Readonly property cannot be reset after cloning when there is no custom clone handler
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 wrongCloneOld()
13+
{
14+
$instance = clone $this;
15+
$this->bar++;
16+
}
17+
18+
public function wrongCloneNew()
19+
{
20+
$instance = clone $this;
21+
$instance->baz++;
22+
}
23+
}
24+
25+
$foo = new Foo(1, 1);
26+
27+
try {
28+
$foo->wrongCloneOld();
29+
} catch (Error $exception) {
30+
echo $exception->getMessage() . "\n";
31+
}
32+
33+
try {
34+
$foo->wrongCloneNew();
35+
} catch (Error $exception) {
36+
echo $exception->getMessage() . "\n";
37+
}
38+
39+
?>
40+
--EXPECT--
41+
Cannot modify readonly property Foo::$bar
42+
Cannot modify readonly property Foo::$baz
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Readonly property cannot be reset after cloning when there is a custom clone handler
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+
public function wrongCloneOld()
15+
{
16+
$instance = clone $this;
17+
$this->bar++;
18+
}
19+
20+
public function wrongCloneNew()
21+
{
22+
$instance = clone $this;
23+
$instance->baz++;
24+
}
25+
}
26+
27+
$foo = new Foo(1, 1);
28+
29+
try {
30+
$foo->wrongCloneOld();
31+
} catch (Error $exception) {
32+
echo $exception->getMessage() . "\n";
33+
}
34+
35+
try {
36+
$foo->wrongCloneNew();
37+
} catch (Error $exception) {
38+
echo $exception->getMessage() . "\n";
39+
}
40+
41+
?>
42+
--EXPECT--
43+
Cannot modify readonly property Foo::$bar
44+
Cannot modify readonly property Foo::$baz
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
) {}
10+
11+
public function __clone()
12+
{
13+
$this->bar++;
14+
}
15+
}
16+
17+
$foo = new Foo(1);
18+
19+
var_dump(clone $foo);
20+
21+
$foo2 = clone $foo;
22+
var_dump($foo2);
23+
24+
var_dump(clone $foo2);
25+
26+
?>
27+
--EXPECTF--
28+
object(Foo)#%d (%d) {
29+
["bar"]=>
30+
int(2)
31+
}
32+
object(Foo)#%d (%d) {
33+
["bar"]=>
34+
int(2)
35+
}
36+
object(Foo)#%d (%d) {
37+
["bar"]=>
38+
int(3)
39+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
--TEST--
2+
Test that __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_enum.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case
4343

4444
ZVAL_STR_COPY(OBJ_PROP_NUM(zobj, 0), case_name);
4545
if (backing_value_zv != NULL) {
46-
ZVAL_COPY(OBJ_PROP_NUM(zobj, 1), backing_value_zv);
46+
zval *prop = OBJ_PROP_NUM(zobj, 1);
47+
48+
ZVAL_COPY(prop, backing_value_zv);
49+
Z_PROP_FLAG_P(prop) = 0;
4750
}
4851

4952
return zobj;
@@ -179,7 +182,7 @@ void zend_enum_add_interfaces(zend_class_entry *ce)
179182

180183
if (ce->enum_backing_type != IS_UNDEF) {
181184
ce->interface_names[num_interfaces_before + 1].name = zend_string_copy(zend_ce_backed_enum->name);
182-
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);
185+
ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0);
183186
}
184187

185188
ce->default_object_handlers = &zend_enum_object_handlers;

Zend/zend_execute.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,12 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
984984
zval tmp;
985985

986986
if (UNEXPECTED(info->flags & ZEND_ACC_READONLY)) {
987-
zend_readonly_property_modification_error(info);
988-
return &EG(uninitialized_zval);
987+
if (Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE) {
988+
Z_PROP_FLAG_P(property_val) &= ~IS_PROP_REINITABLE;
989+
} else {
990+
zend_readonly_property_modification_error(info);
991+
return &EG(uninitialized_zval);
992+
}
989993
}
990994

991995
ZVAL_DEREF(value);
@@ -3125,6 +3129,9 @@ static zend_always_inline void zend_fetch_property_address(zval *result, zval *c
31253129
ZEND_ASSERT(type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET);
31263130
if (Z_TYPE_P(ptr) == IS_OBJECT) {
31273131
ZVAL_COPY(result, ptr);
3132+
} else if (Z_PROP_FLAG_P(ptr) & IS_PROP_REINITABLE) {
3133+
Z_PROP_FLAG_P(ptr) &= ~IS_PROP_REINITABLE;
3134+
ZVAL_COPY(result, ptr);
31283135
} else {
31293136
zend_readonly_property_modification_error(prop_info);
31303137
ZVAL_ERROR(result);

Zend/zend_object_handlers.c

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,8 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
615615
* to make sure no actual modification is possible. */
616616
ZVAL_COPY(rv, retval);
617617
retval = rv;
618+
} else if (Z_PROP_FLAG_P(retval) & IS_PROP_REINITABLE) {
619+
Z_PROP_FLAG_P(retval) &= ~IS_PROP_REINITABLE;
618620
} else {
619621
zend_readonly_property_modification_error(prop_info);
620622
retval = &EG(uninitialized_zval);
@@ -633,7 +635,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
633635
}
634636
}
635637
}
636-
if (UNEXPECTED(Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
638+
if (UNEXPECTED(Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT)) {
637639
/* Skip __get() for uninitialized typed properties */
638640
goto uninit_error;
639641
}
@@ -811,10 +813,14 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
811813

812814
if (UNEXPECTED(prop_info)) {
813815
if (UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
814-
Z_TRY_DELREF_P(value);
815-
zend_readonly_property_modification_error(prop_info);
816-
variable_ptr = &EG(error_zval);
817-
goto exit;
816+
if (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE) {
817+
Z_PROP_FLAG_P(variable_ptr) &= ~IS_PROP_REINITABLE;
818+
} else {
819+
Z_TRY_DELREF_P(value);
820+
zend_readonly_property_modification_error(prop_info);
821+
variable_ptr = &EG(error_zval);
822+
goto exit;
823+
}
818824
}
819825

820826
ZVAL_COPY_VALUE(&tmp, value);
@@ -831,7 +837,7 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
831837
variable_ptr, value, IS_TMP_VAR, property_uses_strict_types());
832838
goto exit;
833839
}
834-
if (Z_PROP_FLAG_P(variable_ptr) == IS_PROP_UNINIT) {
840+
if (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT) {
835841
/* Writes to uninitialized typed properties bypass __set(). */
836842
goto write_std_property;
837843
}
@@ -1049,7 +1055,7 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
10491055
if (UNEXPECTED(Z_TYPE_P(retval) == IS_UNDEF)) {
10501056
if (EXPECTED(!zobj->ce->__get) ||
10511057
UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET) ||
1052-
UNEXPECTED(prop_info && Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
1058+
UNEXPECTED(prop_info && (Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT))) {
10531059
if (UNEXPECTED(type == BP_VAR_RW || type == BP_VAR_R)) {
10541060
if (UNEXPECTED(prop_info)) {
10551061
zend_throw_error(NULL,
@@ -1126,8 +1132,12 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
11261132

11271133
if (Z_TYPE_P(slot) != IS_UNDEF) {
11281134
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY))) {
1129-
zend_readonly_property_unset_error(prop_info->ce, name);
1130-
return;
1135+
if (Z_PROP_FLAG_P(slot) & IS_PROP_REINITABLE) {
1136+
Z_PROP_FLAG_P(slot) &= ~IS_PROP_REINITABLE;
1137+
} else {
1138+
zend_readonly_property_unset_error(prop_info->ce, name);
1139+
return;
1140+
}
11311141
}
11321142
if (UNEXPECTED(Z_ISREF_P(slot)) &&
11331143
(ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(slot)))) {
@@ -1144,7 +1154,7 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
11441154
}
11451155
return;
11461156
}
1147-
if (UNEXPECTED(Z_PROP_FLAG_P(slot) == IS_PROP_UNINIT)) {
1157+
if (UNEXPECTED(Z_PROP_FLAG_P(slot) & IS_PROP_UNINIT)) {
11481158
if (UNEXPECTED(prop_info && (prop_info->flags & ZEND_ACC_READONLY)
11491159
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "unset"))) {
11501160
return;
@@ -1759,7 +1769,7 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has
17591769
if (Z_TYPE_P(value) != IS_UNDEF) {
17601770
goto found;
17611771
}
1762-
if (UNEXPECTED(Z_PROP_FLAG_P(value) == IS_PROP_UNINIT)) {
1772+
if (UNEXPECTED(Z_PROP_FLAG_P(value) & IS_PROP_UNINIT)) {
17631773
/* Skip __isset() for uninitialized typed properties */
17641774
result = 0;
17651775
goto exit;

0 commit comments

Comments
 (0)