Skip to content

Disallow assigning reference to unset readonly property #8188

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

Closed
wants to merge 1 commit into from
Closed
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
13 changes: 7 additions & 6 deletions Zend/tests/readonly_props/array_append_initialization.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ function init() {
var_dump($c->a);
}

(new C)->init();
try {
(new C)->init();
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

try {
init();
Expand All @@ -29,8 +33,5 @@ try {

?>
--EXPECT--
array(1) {
[0]=>
int(1)
}
Cannot initialize readonly property C::$a from global scope
Cannot indirectly modify readonly property C::$a
Cannot indirectly modify readonly property C::$a
22 changes: 22 additions & 0 deletions Zend/tests/readonly_props/gh7942.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
GH-7942: Disallow assigning reference to unset readonly property
--FILE--
<?php

class Foo {
public readonly int $bar;
public function __construct(int &$bar) {
$this->bar = &$bar;
}
}

try {
$i = 42;
new Foo($i);
} catch (Error $e) {
echo $e->getMessage(), PHP_EOL;
}

?>
--EXPECT--
Cannot indirectly modify readonly property Foo::$bar
118 changes: 118 additions & 0 deletions Zend/tests/readonly_props/variation.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
--TEST--
Readonly variations
--FILE--
Comment on lines +1 to +3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a string concatenation variation maybe? And maybe ++, -- as IIRC those are different opcodes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+= and .= both result in ASSIGN_OP. ++ might be worth checking. I'll adjust the test.

<?php

class Test {
public readonly int $prop;

public function init() {
$this->prop = 1;
}

public function r() {
echo $this->prop;
}

public function w() {
$this->prop = 1;
echo 'done';
}

public function rw() {
$this->prop += 1;
echo 'done';
}

public function im() {
$this->prop[] = 1;
echo 'done';
}

public function is() {
echo (int) isset($this->prop);
}

public function us() {
unset($this->prop);
echo 'done';
}
}

function r($test) {
echo $test->prop;
}

function w($test) {
$test->prop = 0;
echo 'done';
}

function rw($test) {
$test->prop += 1;
echo 'done';
}

function im($test) {
$test->prop[] = 1;
echo 'done';
}

function is($test) {
echo (int) isset($test->prop);
}

function us($test) {
unset($test->prop);
echo 'done';
}

foreach ([true, false] as $init) {
foreach ([true, false] as $scope) {
foreach (['r', 'w', 'rw', 'im', 'is', 'us'] as $op) {
$test = new Test();
if ($init) {
$test->init();
}

echo 'Init: ' . ((int) $init) . ', scope: ' . ((int) $scope) . ', op: ' . $op . ": ";
try {
if ($scope) {
$test->{$op}();
} else {
$op($test);
}
} catch (Error $e) {
echo $e->getMessage();
}
echo "\n";
}
}
}

?>
--EXPECT--
Init: 1, scope: 1, op: r: 1
Init: 1, scope: 1, op: w: Cannot modify readonly property Test::$prop
Init: 1, scope: 1, op: rw: Cannot modify readonly property Test::$prop
Init: 1, scope: 1, op: im: Cannot modify readonly property Test::$prop
Init: 1, scope: 1, op: is: 1
Init: 1, scope: 1, op: us: Cannot unset readonly property Test::$prop
Init: 1, scope: 0, op: r: 1
Init: 1, scope: 0, op: w: Cannot modify readonly property Test::$prop
Init: 1, scope: 0, op: rw: Cannot modify readonly property Test::$prop
Init: 1, scope: 0, op: im: Cannot modify readonly property Test::$prop
Init: 1, scope: 0, op: is: 1
Init: 1, scope: 0, op: us: Cannot unset readonly property Test::$prop
Init: 0, scope: 1, op: r: Typed property Test::$prop must not be accessed before initialization
Init: 0, scope: 1, op: w: done
Init: 0, scope: 1, op: rw: Typed property Test::$prop must not be accessed before initialization
Init: 0, scope: 1, op: im: Cannot indirectly modify readonly property Test::$prop
Init: 0, scope: 1, op: is: 0
Init: 0, scope: 1, op: us: done
Init: 0, scope: 0, op: r: Typed property Test::$prop must not be accessed before initialization
Init: 0, scope: 0, op: w: Cannot initialize readonly property Test::$prop from global scope
Init: 0, scope: 0, op: rw: Typed property Test::$prop must not be accessed before initialization
Init: 0, scope: 0, op: im: Cannot indirectly modify readonly property Test::$prop
Init: 0, scope: 0, op: is: 0
Init: 0, scope: 0, op: us: Cannot unset readonly property Test::$prop from global scope
77 changes: 77 additions & 0 deletions Zend/tests/readonly_props/variation_nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
Readonly nested variations
--FILE--
<?php

class Inner {
public int $prop = 1;
public array $array = [];
}

class Test {
public readonly Inner $prop;

public function init() {
$this->prop = new Inner();
}
}

function r($test) {
echo $test->prop->prop;
}

function w($test) {
$test->prop->prop = 0;
echo 'done';
}

function rw($test) {
$test->prop->prop += 1;
echo 'done';
}

function im($test) {
$test->prop->array[] = 1;
echo 'done';
}

function is($test) {
echo (int) isset($test->prop->prop);
}

function us($test) {
unset($test->prop->prop);
echo 'done';
}

foreach ([true, false] as $init) {
foreach (['r', 'w', 'rw', 'im', 'is', 'us'] as $op) {
$test = new Test();
if ($init) {
$test->init();
}

echo 'Init: ' . ((int) $init) . ', op: ' . $op . ": ";
try {
$op($test);
} catch (Error $e) {
echo $e->getMessage();
}
echo "\n";
}
}

?>
--EXPECT--
Init: 1, op: r: 1
Init: 1, op: w: done
Init: 1, op: rw: done
Init: 1, op: im: done
Init: 1, op: is: 1
Init: 1, op: us: done
Init: 0, op: r: Typed property Test::$prop must not be accessed before initialization
Init: 0, op: w: Cannot indirectly modify readonly property Test::$prop
Init: 0, op: rw: Typed property Test::$prop must not be accessed before initialization
Init: 0, op: im: Cannot indirectly modify readonly property Test::$prop
Init: 0, op: is: 0
Init: 0, op: us: done
6 changes: 6 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,12 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(
ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name));
}

ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_indirect_modification_error(zend_property_info *info)
{
zend_throw_error(NULL, "Cannot indirectly modify readonly property %s::$%s",
ZSTR_VAL(info->ce->name), zend_get_unmangled_property_name(info->name));
}

static zend_class_entry *resolve_single_class_type(zend_string *name, zend_class_entry *self_ce) {
if (zend_string_equals_literal_ci(name, "self")) {
return self_ce;
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht,
ZEND_API ZEND_COLD void zend_wrong_string_offset_error(void);

ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_modification_error(zend_property_info *info);
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_readonly_property_indirect_modification_error(zend_property_info *info);

ZEND_API bool zend_verify_scalar_type_hint(uint32_t type_mask, zval *arg, bool strict, bool is_internal_arg);
ZEND_API ZEND_COLD void zend_verify_arg_error(
Expand Down
17 changes: 14 additions & 3 deletions Zend/zend_object_handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,17 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
}
}
goto exit;
} else {
if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
if (type == BP_VAR_W || type == BP_VAR_RW) {
zend_readonly_property_indirect_modification_error(prop_info);
retval = &EG(uninitialized_zval);
goto exit;
} else if (type == BP_VAR_UNSET) {
retval = &EG(uninitialized_zval);
goto exit;
}
}
}
if (UNEXPECTED(Z_PROP_FLAG_P(retval) == IS_PROP_UNINIT)) {
/* Skip __get() for uninitialized typed properties */
Expand Down Expand Up @@ -1024,9 +1035,9 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam
ZVAL_NULL(retval);
zend_error(E_WARNING, "Undefined property: %s::$%s", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name));
}
} else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)
&& !verify_readonly_initialization_access(prop_info, zobj->ce, name, "initialize")) {
retval = &EG(error_zval);
} else if (prop_info && UNEXPECTED(prop_info->flags & ZEND_ACC_READONLY)) {
/* Readonly property, delegate to read_property + write_property. */
retval = NULL;
}
} else {
/* we do have getter - fail and let it try again with usual get/set */
Expand Down
7 changes: 3 additions & 4 deletions ext/opcache/tests/jit/fetch_obj_006.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ $appendProp2 = (function() {
$this->prop[] = 1;
})->bindTo($test, Test::class);
$appendProp2();
$appendProp2();
?>
--EXPECTF--
Fatal error: Uncaught Error: Cannot modify readonly property Test::$prop in %sfetch_obj_006.php:13
Fatal error: Uncaught Error: Cannot indirectly modify readonly property Test::$prop in %s:%d
Stack trace:
#0 %sfetch_obj_006.php(16): Test->{closure}()
#0 %sfetch_obj_006.php(15): Test->{closure}()
#1 {main}
thrown in %sfetch_obj_006.php on line 13
thrown in %sfetch_obj_006.php on line 13