Skip to content

Allow readonly properties to be reinitialized once during cloning #10389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_error1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
Readonly property cannot be reset twice during cloning
--FILE--
<?php

class Foo {
public function __construct(
public readonly int $bar
) {}

public function __clone()
{
$this->bar = 2;
var_dump($this);
$this->bar = 3;
}
}

$foo = new Foo(1);

try {
clone $foo;
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

echo "done";

?>
--EXPECT--
object(Foo)#2 (1) {
["bar"]=>
int(2)
}
Cannot modify readonly property Foo::$bar
done
42 changes: 42 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_error2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
Readonly property cannot be reset after cloning when there is no custom clone handler
--FILE--
<?php

class Foo {
public function __construct(
public readonly int $bar,
public readonly int $baz
) {}

public function wrongCloneOld()
{
$instance = clone $this;
$this->bar++;
}

public function wrongCloneNew()
{
$instance = clone $this;
$instance->baz++;
}
}

$foo = new Foo(1, 1);

try {
$foo->wrongCloneOld();
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

try {
$foo->wrongCloneNew();
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

?>
--EXPECT--
Cannot modify readonly property Foo::$bar
Cannot modify readonly property Foo::$baz
44 changes: 44 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_error3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
Readonly property cannot be reset after cloning when there is a custom clone handler
--FILE--
<?php

class Foo {
public function __construct(
public readonly int $bar,
public readonly int $baz
) {}

public function __clone() {}

public function wrongCloneOld()
{
$instance = clone $this;
$this->bar++;
}

public function wrongCloneNew()
{
$instance = clone $this;
$instance->baz++;
}
}

$foo = new Foo(1, 1);

try {
$foo->wrongCloneOld();
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

try {
$foo->wrongCloneNew();
} catch (Error $exception) {
echo $exception->getMessage() . "\n";
}

?>
--EXPECT--
Cannot modify readonly property Foo::$bar
Cannot modify readonly property Foo::$baz
39 changes: 39 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_success1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Readonly property can be reset once during cloning
--FILE--
<?php

class Foo {
public function __construct(
public readonly int $bar
) {}

public function __clone()
{
$this->bar++;
}
}

$foo = new Foo(1);

var_dump(clone $foo);

$foo2 = clone $foo;
var_dump($foo2);

var_dump(clone $foo2);

?>
--EXPECTF--
object(Foo)#%d (%d) {
["bar"]=>
int(2)
}
object(Foo)#%d (%d) {
["bar"]=>
int(2)
}
object(Foo)#%d (%d) {
["bar"]=>
int(3)
}
46 changes: 46 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_success2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
Test that __clone() unset and reassign properties
--FILE--
<?php

class Foo {
public function __construct(
public readonly stdClass $bar,
) {}

public function __clone()
{
unset($this->bar);
var_dump($this);
$this->bar = new stdClass();
}
}

$foo = new Foo(new stdClass());
var_dump($foo);
$foo2 = clone $foo;

var_dump($foo);
var_dump($foo2);

?>
--EXPECTF--
object(Foo)#1 (%d) {
["bar"]=>
object(stdClass)#2 (%d) {
}
}
object(Foo)#3 (%d) {
["bar"]=>
uninitialized(stdClass)
}
object(Foo)#1 (%d) {
["bar"]=>
object(stdClass)#2 (%d) {
}
}
object(Foo)#3 (%d) {
["bar"]=>
object(stdClass)#4 (%d) {
}
}
37 changes: 37 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_success3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
__clone() can indirectly modify unlocked readonly properties
--FILE--
<?php

class Foo {
public function __construct(
public readonly array $bar
) {}

public function __clone()
{
$this->bar['bar'] = 'bar';
}
}

$foo = new Foo([]);
// First call fills the cache slot
var_dump(clone $foo);
var_dump(clone $foo);

?>
--EXPECTF--
object(Foo)#2 (%d) {
["bar"]=>
array(1) {
["bar"]=>
string(3) "bar"
}
}
object(Foo)#2 (%d) {
["bar"]=>
array(1) {
["bar"]=>
string(3) "bar"
}
}
39 changes: 39 additions & 0 deletions Zend/tests/readonly_props/readonly_clone_success4.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Readonly property can be reset once during cloning even after a type error
--FILE--
<?php

class Foo {
public function __construct(
public readonly int $bar
) {}

public function __clone()
{
try {
$this->bar = "foo";
} catch (Error $e) {
echo $e->getMessage() . "\n";
}

$this->bar = 2;
}
}

$foo = new Foo(1);

var_dump(clone $foo);
var_dump(clone $foo);

?>
--EXPECTF--
Cannot assign string to property Foo::$bar of type int
object(Foo)#%d (%d) {
["bar"]=>
int(2)
}
Cannot assign string to property Foo::$bar of type int
object(Foo)#%d (%d) {
["bar"]=>
int(2)
}
23 changes: 23 additions & 0 deletions Zend/tests/readonly_props/readonly_coercion_type_error.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Test failing readonly assignment with coercion
--FILE--
<?php

class Foo {
public readonly string $bar;

public function __construct() {
$this->bar = 'bar';
try {
$this->bar = 42;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
}

new Foo();

?>
--EXPECTF--
Cannot modify readonly property Foo::$bar
4 changes: 4 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -4224,6 +4224,10 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z
ce->ce_flags |= ZEND_ACC_HAS_TYPE_HINTS;
}

if (access_type & ZEND_ACC_READONLY) {
ce->ce_flags |= ZEND_ACC_HAS_READONLY_PROPS;
}

if (ce->type == ZEND_INTERNAL_CLASS) {
property_info = pemalloc(sizeof(zend_property_info), 1);
} else {
Expand Down
4 changes: 3 additions & 1 deletion Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ typedef struct _zend_oparray_context {
/* or IS_CONSTANT_VISITED_MARK | | | */
#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */
/* | | | */
/* Class Flags (unused: 21,30,31) | | | */
/* Class Flags (unused: 30,31) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
Expand Down Expand Up @@ -288,6 +288,8 @@ typedef struct _zend_oparray_context {
/* | | | */
/* Class is linked apart from variance obligations. | | | */
#define ZEND_ACC_NEARLY_LINKED (1 << 20) /* X | | | */
/* Class has readonly props | | | */
#define ZEND_ACC_HAS_READONLY_PROPS (1 << 21) /* X | | | */
/* | | | */
/* stored in opcache (may be partially) | | | */
#define ZEND_ACC_CACHED (1 << 22) /* X | | | */
Expand Down
14 changes: 11 additions & 3 deletions Zend/zend_enum.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,17 @@ zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case
zend_object *zobj = zend_objects_new(ce);
ZVAL_OBJ(result, zobj);

ZVAL_STR_COPY(OBJ_PROP_NUM(zobj, 0), case_name);
zval *zname = OBJ_PROP_NUM(zobj, 0);
ZVAL_STR_COPY(zname, case_name);
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
Z_PROP_FLAG_P(zname) = 0;

if (backing_value_zv != NULL) {
ZVAL_COPY(OBJ_PROP_NUM(zobj, 1), backing_value_zv);
zval *prop = OBJ_PROP_NUM(zobj, 1);

ZVAL_COPY(prop, backing_value_zv);
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
Z_PROP_FLAG_P(prop) = 0;
}

return zobj;
Expand Down Expand Up @@ -179,7 +187,7 @@ void zend_enum_add_interfaces(zend_class_entry *ce)

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

ce->default_object_handlers = &zend_enum_object_handlers;
Expand Down
Loading