Skip to content

Commit 8fbc8e7

Browse files
committed
Disallow assigning reference to unset readonly property
Closes GH-7942
1 parent 9458f5f commit 8fbc8e7

File tree

7 files changed

+168
-13
lines changed

7 files changed

+168
-13
lines changed

Zend/tests/readonly_props/array_append_initialization.phpt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ function init() {
1919
var_dump($c->a);
2020
}
2121

22-
(new C)->init();
22+
try {
23+
(new C)->init();
24+
} catch (Error $e) {
25+
echo $e->getMessage(), "\n";
26+
}
2327

2428
try {
2529
init();
@@ -29,8 +33,5 @@ try {
2933

3034
?>
3135
--EXPECT--
32-
array(1) {
33-
[0]=>
34-
int(1)
35-
}
36-
Cannot initialize readonly property C::$a from global scope
36+
Cannot indirectly modify readonly property C::$a
37+
Cannot indirectly modify readonly property C::$a

Zend/tests/readonly_props/gh7942.phpt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
GH-7942: Disallow assigning reference to unset readonly property
3+
--FILE--
4+
<?php
5+
6+
class Foo {
7+
public readonly int $bar;
8+
public function __construct(int &$bar) {
9+
$this->bar = &$bar;
10+
}
11+
}
12+
13+
try {
14+
$i = 42;
15+
new Foo($i);
16+
} catch (Error $e) {
17+
echo $e->getMessage(), PHP_EOL;
18+
}
19+
20+
?>
21+
--EXPECT--
22+
Cannot indirectly modify readonly property Foo::$bar
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
--TEST--
2+
Readonly variations
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public readonly int $prop;
8+
9+
public function init() {
10+
$this->prop = 1;
11+
}
12+
13+
public function r() {
14+
echo $this->prop;
15+
}
16+
17+
public function w() {
18+
$this->prop = 1;
19+
echo 'done';
20+
}
21+
22+
public function rw() {
23+
$this->prop += 1;
24+
echo 'done';
25+
}
26+
27+
public function im() {
28+
$this->prop[] = 1;
29+
echo 'done';
30+
}
31+
32+
public function is() {
33+
echo (int) isset($this->prop);
34+
}
35+
36+
public function us() {
37+
unset($this->prop);
38+
echo 'done';
39+
}
40+
}
41+
42+
function r($test) {
43+
echo $test->prop;
44+
}
45+
46+
function w($test) {
47+
$test->prop = 0;
48+
echo 'done';
49+
}
50+
51+
function rw($test) {
52+
$test->prop += 1;
53+
echo 'done';
54+
}
55+
56+
function im($test) {
57+
$test->prop[] = 1;
58+
echo 'done';
59+
}
60+
61+
function is($test) {
62+
echo (int) isset($test->prop);
63+
}
64+
65+
function us($test) {
66+
unset($test->prop);
67+
echo 'done';
68+
}
69+
70+
foreach ([true, false] as $init) {
71+
foreach ([true, false] as $scope) {
72+
foreach (['r', 'w', 'rw', 'im', 'is', 'us'] as $op) {
73+
$test = new Test();
74+
if ($init) {
75+
$test->init();
76+
}
77+
78+
echo 'Init: ' . ((int) $init) . ', scope: ' . ((int) $scope) . ', op: ' . $op . ": ";
79+
try {
80+
if ($scope) {
81+
$test->{$op}();
82+
} else {
83+
$op($test);
84+
}
85+
} catch (Error $e) {
86+
echo $e->getMessage();
87+
}
88+
echo "\n";
89+
}
90+
}
91+
}
92+
93+
?>
94+
--EXPECT--
95+
Init: 1, scope: 1, op: r: 1
96+
Init: 1, scope: 1, op: w: Cannot modify readonly property Test::$prop
97+
Init: 1, scope: 1, op: rw: Cannot modify readonly property Test::$prop
98+
Init: 1, scope: 1, op: im: Cannot modify readonly property Test::$prop
99+
Init: 1, scope: 1, op: is: 1
100+
Init: 1, scope: 1, op: us: Cannot unset readonly property Test::$prop
101+
Init: 1, scope: 0, op: r: 1
102+
Init: 1, scope: 0, op: w: Cannot modify readonly property Test::$prop
103+
Init: 1, scope: 0, op: rw: Cannot modify readonly property Test::$prop
104+
Init: 1, scope: 0, op: im: Cannot modify readonly property Test::$prop
105+
Init: 1, scope: 0, op: is: 1
106+
Init: 1, scope: 0, op: us: Cannot unset readonly property Test::$prop
107+
Init: 0, scope: 1, op: r: Typed property Test::$prop must not be accessed before initialization
108+
Init: 0, scope: 1, op: w: done
109+
Init: 0, scope: 1, op: rw: Typed property Test::$prop must not be accessed before initialization
110+
Init: 0, scope: 1, op: im: Cannot indirectly modify readonly property Test::$prop
111+
Init: 0, scope: 1, op: is: 0
112+
Init: 0, scope: 1, op: us: done
113+
Init: 0, scope: 0, op: r: Typed property Test::$prop must not be accessed before initialization
114+
Init: 0, scope: 0, op: w: Cannot initialize readonly property Test::$prop from global scope
115+
Init: 0, scope: 0, op: rw: Typed property Test::$prop must not be accessed before initialization
116+
Init: 0, scope: 0, op: im: Cannot indirectly modify readonly property Test::$prop
117+
Init: 0, scope: 0, op: is: 0
118+
Init: 0, scope: 0, op: us: Cannot unset readonly property Test::$prop from global scope

Zend/zend_execute.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,12 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(
853853
ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name));
854854
}
855855

856+
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_indirect_modification_error(zend_property_info *info)
857+
{
858+
zend_throw_error(NULL, "Cannot indirectly modify readonly property %s::$%s",
859+
ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name));
860+
}
861+
856862
static zend_class_entry *resolve_single_class_type(zend_string *name, zend_class_entry *self_ce) {
857863
if (zend_string_equals_literal_ci(name, "self")) {
858864
return self_ce;

Zend/zend_execute.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht,
7474
ZEND_API ZEND_COLD void zend_wrong_string_offset_error(void);
7575

7676
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(zend_property_info *info);
77+
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_indirect_modification_error(zend_property_info *info);
7778

7879
ZEND_API bool zend_verify_scalar_type_hint(uint32_t type_mask, zval *arg, bool strict, bool is_internal_arg);
7980
ZEND_API ZEND_COLD void zend_verify_arg_error(

Zend/zend_object_handlers.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,14 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
622622
}
623623
}
624624
goto exit;
625+
} else {
626+
if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
627+
if (type == BP_VAR_W || type == BP_VAR_RW) {
628+
zend_readonly_property_indirect_modification_error(prop_info);
629+
retval = &EG(uninitialized_zval);
630+
goto exit;
631+
}
632+
}
625633
}
626634
if (UNEXPECTED(Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
627635
/* Skip __get() for uninitialized typed properties */
@@ -1051,9 +1059,9 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
10511059
ZVAL_NULL(retval);
10521060
zend_error(E_WARNING, "Undefined property: %s::$%s", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name));
10531061
}
1054-
} else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)
1055-
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "initialize")) {
1056-
retval = &EG(error_zval);
1062+
} else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
1063+
/* Readonly property, delegate to read_property + write_property. */
1064+
retval = NULL;
10571065
}
10581066
} else {
10591067
/* we do have getter - fail and let it try again with usual get/set */

ext/opcache/tests/jit/fetch_obj_006.phpt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ $appendProp2 = (function() {
2121
$this->prop[] = 1;
2222
})->bindTo($test, Test::class);
2323
$appendProp2();
24-
$appendProp2();
2524
?>
2625
--EXPECTF--
27-
Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in %sfetch_obj_006.php:13
26+
Fatal error: Uncaught Error: Typed property Test::$prop must not be accessed before initialization in %sfetch_obj_006.php:13
2827
Stack trace:
29-
#0 %sfetch_obj_006.php(16): Test->{closure}()
28+
#0 %sfetch_obj_006.php(15): Test->{closure}()
3029
#1 {main}
31-
thrown in %sfetch_obj_006.php on line 13
30+
thrown in %sfetch_obj_006.php on line 13

0 commit comments

Comments
 (0)