diff --git a/Zend/tests/readonly_props/cpp_reassign_basic.phpt b/Zend/tests/readonly_props/cpp_reassign_basic.phpt new file mode 100644 index 0000000000000..c1b68761bd911 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_basic.phpt @@ -0,0 +1,28 @@ +--TEST-- +Promoted readonly property reassignment in constructor - basic +--FILE-- +x = abs($x); + $this->y = abs($y); + } +} + +$point = new Point(); +var_dump($point->x, $point->y); + +$point2 = new Point(-5, -3); +var_dump($point2->x, $point2->y); + +?> +--EXPECT-- +int(0) +int(0) +int(5) +int(3) diff --git a/Zend/tests/readonly_props/cpp_reassign_child_class.phpt b/Zend/tests/readonly_props/cpp_reassign_child_class.phpt new file mode 100644 index 0000000000000..d054585861aa3 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_child_class.phpt @@ -0,0 +1,89 @@ +--TEST-- +Promoted readonly property reassignment in constructor - child cannot reassign parent's property +--FILE-- +prop = 'child override'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$child = new Child1(); +var_dump($child->prop); + +// Case 2: Parent USES reassignment, child cannot +class Parent2 { + public function __construct( + public readonly string $prop = 'parent default', + ) { + $this->prop = 'parent set'; // Uses the one reassignment + } +} + +class Child2 extends Parent2 { + public function __construct() { + parent::__construct(); + // Child cannot reassign parent-owned promoted property + try { + $this->prop = 'child override'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$child2 = new Child2(); +var_dump($child2->prop); + +// Case 3: Child with its own promoted property +class Parent3 { + public function __construct( + public readonly string $parentProp = 'parent default', + ) { + // Parent does NOT reassign here + } +} + +class Child3 extends Parent3 { + public function __construct( + public readonly string $childProp = 'child default', + ) { + parent::__construct(); + // Child cannot reassign parent's property, but can reassign its own + try { + $this->parentProp = 'child set parent'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + $this->childProp = 'child set own'; + } +} + +$child3 = new Child3(); +var_dump($child3->parentProp, $child3->childProp); + +?> +--EXPECT-- +Error: Cannot modify readonly property Parent1::$prop +string(14) "parent default" +Error: Cannot modify readonly property Parent2::$prop +string(10) "parent set" +Error: Cannot modify readonly property Parent3::$parentProp +string(14) "parent default" +string(13) "child set own" diff --git a/Zend/tests/readonly_props/cpp_reassign_child_preempt_parent.phpt b/Zend/tests/readonly_props/cpp_reassign_child_preempt_parent.phpt new file mode 100644 index 0000000000000..db2bb81c9eb10 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_child_preempt_parent.phpt @@ -0,0 +1,31 @@ +--TEST-- +Promoted readonly property reassignment in constructor - child preempt then parent ctor throws +--FILE-- +prop = 'parent set'; + } +} + +class ChildCPP extends ParentCPP { + public function __construct() { + $this->prop = 'child set'; + try { + parent::__construct(); + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$c = new ChildCPP(); +var_dump($c->prop); + +?> +--EXPECT-- +Error: Cannot modify readonly property ParentCPP::$prop +string(9) "child set" diff --git a/Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt b/Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt new file mode 100644 index 0000000000000..1cd82db84f528 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_child_redefine.phpt @@ -0,0 +1,84 @@ +--TEST-- +Promoted readonly property reassignment in constructor - child redefines parent property +--FILE-- +x = 'C'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$c1 = new C1(); +var_dump($c1->x); + +// Case 2: Parent uses CPP and reassigns; child redefines as non-promoted. +// The child does not use CPP, so it does not claim CPP ownership of the property. +// P2's CPP "owns" the reassignment window: P2's body write succeeds. +class P2 { + public function __construct( + public readonly string $x = 'P1', + ) { + $this->x = 'P2'; + } +} + +class C2 extends P2 { + public readonly string $x; + + public function __construct() { + parent::__construct(); + } +} + +$c2 = new C2(); +var_dump($c2->x); + +// Case 3: Parent uses CPP, child uses CPP redefinition. +// Child's CPP opens the reassignment window for C3::$x. When parent::__construct() +// runs, P3's CPP tries to initialize C3::$x again, which must fail since C3 +// owns the property and has already initialized it. +class P3 { + public function __construct( + public readonly string $x = 'P', + ) {} +} + +class C3 extends P3 { + public function __construct( + public readonly string $x = 'C1', + ) { + try { + parent::__construct(); + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$c3 = new C3(); +var_dump($c3->x); + +?> +--EXPECT-- +Error: Cannot modify readonly property C1::$x +string(1) "P" +string(2) "P2" +Error: Cannot modify readonly property C3::$x +string(2) "C1" diff --git a/Zend/tests/readonly_props/cpp_reassign_conditional.phpt b/Zend/tests/readonly_props/cpp_reassign_conditional.phpt new file mode 100644 index 0000000000000..e67177e76e3c2 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_conditional.phpt @@ -0,0 +1,23 @@ +--TEST-- +Promoted readonly property reassignment in constructor - conditional initialization +--FILE-- +cacheDir ??= '/tmp/app_cache'; + } +} + +$config1 = new Config(); +var_dump($config1->cacheDir); + +$config2 = new Config('/custom/cache'); +var_dump($config2->cacheDir); + +?> +--EXPECT-- +string(14) "/tmp/app_cache" +string(13) "/custom/cache" diff --git a/Zend/tests/readonly_props/cpp_reassign_different_object.phpt b/Zend/tests/readonly_props/cpp_reassign_different_object.phpt new file mode 100644 index 0000000000000..293826f2ee13a --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_different_object.phpt @@ -0,0 +1,34 @@ +--TEST-- +Promoted readonly property reassignment in constructor - different object fails +--FILE-- +x = $x * 2; + if ($other !== null) { + try { + $other->x = 999; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } + } +} + +$a = new Foo(5); +var_dump($a->x); + +$b = new Foo(3, $a); +var_dump($a->x, $b->x); // $a unchanged + +?> +--EXPECT-- +int(10) +Error: Cannot modify readonly property Foo::$x +int(10) +int(6) diff --git a/Zend/tests/readonly_props/cpp_reassign_direct_ctor_call.phpt b/Zend/tests/readonly_props/cpp_reassign_direct_ctor_call.phpt new file mode 100644 index 0000000000000..5ff246eaa46eb --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_direct_ctor_call.phpt @@ -0,0 +1,29 @@ +--TEST-- +Promoted readonly properties cannot be reassigned when __construct() is called directly +--FILE-- +value = strtoupper($this->value); + } +} + +$obj = new Foo('hello'); +var_dump($obj->value); + +// Direct call fails: CPP assignment cannot reinitialize an already-set property +try { + $obj->__construct('world'); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +var_dump($obj->value); + +?> +--EXPECT-- +string(5) "HELLO" +Error: Cannot modify readonly property Foo::$value +string(5) "HELLO" diff --git a/Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt b/Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt new file mode 100644 index 0000000000000..3437d54dffbc5 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_indirect_allowed.phpt @@ -0,0 +1,47 @@ +--TEST-- +Promoted readonly property reassignment in constructor - indirect reassignment allowed +--FILE-- +initProp(); + } + + private function initProp(): void { + $this->prop = 'from method'; + } +} + +$cm = new CalledMethod(); +var_dump($cm->prop); + +// But second reassignment still fails +class MultipleReassign { + public function __construct( + public readonly string $prop = 'default', + ) { + $this->initProp("first from method"); + try { + $this->initProp("second from method"); // Second call - should fail + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } + + private function initProp(string $v): void { + $this->prop = $v; + } +} + +$mr = new MultipleReassign(); +var_dump($mr->prop); + +?> +--EXPECT-- +string(11) "from method" +Error: Cannot modify readonly property MultipleReassign::$prop +string(17) "first from method" diff --git a/Zend/tests/readonly_props/cpp_reassign_indirect_ops.phpt b/Zend/tests/readonly_props/cpp_reassign_indirect_ops.phpt new file mode 100644 index 0000000000000..e39c3e18d8885 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_indirect_ops.phpt @@ -0,0 +1,42 @@ +--TEST-- +Promoted readonly property reassignment in constructor - indirect operations (++, --, +=) +--FILE-- +count++; + } +} + +$c = new Counter(5); +var_dump($c->count); + +// Multiple operations count as reassignments - second fails +class MultiOp { + public function __construct( + public readonly int $value = 10, + ) { + $this->value += 5; // First modification - allowed + try { + $this->value++; // Second modification - should fail + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$m = new MultiOp(); +var_dump($m->value); + +?> +--EXPECT-- +int(6) +Error: Cannot modify readonly property MultiOp::$value +int(15) diff --git a/Zend/tests/readonly_props/cpp_reassign_multiple_fail.phpt b/Zend/tests/readonly_props/cpp_reassign_multiple_fail.phpt new file mode 100644 index 0000000000000..fcbce0a372bae --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_multiple_fail.phpt @@ -0,0 +1,25 @@ +--TEST-- +Promoted readonly property reassignment in constructor - multiple reassignments fail +--FILE-- +value = 'first'; // OK - first reassignment + try { + $this->value = 'second'; // Error - second reassignment + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$ex = new Example(); +var_dump($ex->value); + +?> +--EXPECT-- +Error: Cannot modify readonly property Example::$value +string(5) "first" diff --git a/Zend/tests/readonly_props/cpp_reassign_nonpromoted.phpt b/Zend/tests/readonly_props/cpp_reassign_nonpromoted.phpt new file mode 100644 index 0000000000000..092c496c87936 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_nonpromoted.phpt @@ -0,0 +1,49 @@ +--TEST-- +Promoted readonly property reassignment in constructor - non-promoted properties unchanged +--FILE-- +prop = 'first'; + try { + $this->prop = 'second'; // Should fail - not a promoted property + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$np = new NonPromoted(); +var_dump($np->prop); + +// Test mixed: promoted and non-promoted in same class +class MixedProps { + public readonly string $nonPromoted; + + public function __construct( + public readonly string $promoted = 'default', + ) { + $this->nonPromoted = 'first'; + $this->promoted = 'reassigned'; // Allowed (promoted, first reassignment) + try { + $this->nonPromoted = 'second'; // Should fail (non-promoted) + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$m = new MixedProps(); +var_dump($m->promoted, $m->nonPromoted); + +?> +--EXPECT-- +Error: Cannot modify readonly property NonPromoted::$prop +string(5) "first" +Error: Cannot modify readonly property MixedProps::$nonPromoted +string(10) "reassigned" +string(5) "first" diff --git a/Zend/tests/readonly_props/cpp_reassign_outside_ctor.phpt b/Zend/tests/readonly_props/cpp_reassign_outside_ctor.phpt new file mode 100644 index 0000000000000..f6d0308c49948 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_outside_ctor.phpt @@ -0,0 +1,43 @@ +--TEST-- +Promoted readonly property reassignment in constructor - outside constructor fails +--FILE-- +x = abs($x); + } + + public function tryModify(): void { + $this->x = 999; + } +} + +$point = new Point(-5); +var_dump($point->x); + +// Cannot reassign from outside constructor +try { + $point->x = 100; +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +var_dump($point->x); + +// Cannot reassign from a method +try { + $point->tryModify(); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +var_dump($point->x); + +?> +--EXPECT-- +int(5) +Error: Cannot modify readonly property Point::$x +int(5) +Error: Cannot modify readonly property Point::$x +int(5) diff --git a/Zend/tests/readonly_props/cpp_reassign_reflection.phpt b/Zend/tests/readonly_props/cpp_reassign_reflection.phpt new file mode 100644 index 0000000000000..8f3033e0f13a5 --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_reflection.phpt @@ -0,0 +1,41 @@ +--TEST-- +Promoted readonly property reassignment works when object created via reflection +--FILE-- +bar = strtoupper($bar); + } +} + +$ref = new ReflectionClass(Foo::class); +$obj = $ref->newInstanceWithoutConstructor(); + +// Property is uninitialized at this point +try { + var_dump($obj->bar); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} + +// First constructor call: CPP reassignment allowed +$obj->__construct('explicit call'); +var_dump($obj->bar); + +// Second call fails: property no longer uninitialized +try { + $obj->__construct('second call'); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +var_dump($obj->bar); + +?> +--EXPECT-- +Error: Typed property Foo::$bar must not be accessed before initialization +string(13) "EXPLICIT CALL" +Error: Cannot modify readonly property Foo::$bar +string(13) "EXPLICIT CALL" diff --git a/Zend/tests/readonly_props/cpp_reassign_validation.phpt b/Zend/tests/readonly_props/cpp_reassign_validation.phpt new file mode 100644 index 0000000000000..f8f9609f5773d --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_validation.phpt @@ -0,0 +1,30 @@ +--TEST-- +Promoted readonly property reassignment in constructor - validation +--FILE-- +email = strtolower($email); // Normalize + } +} + +$user = new User('TEST@Example.COM'); +var_dump($user->email); + +try { + new User('not-an-email'); +} catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +string(16) "test@example.com" +ValueError: Invalid email diff --git a/Zend/tests/readonly_props/cpp_reassign_visibility.phpt b/Zend/tests/readonly_props/cpp_reassign_visibility.phpt new file mode 100644 index 0000000000000..340e14478407f --- /dev/null +++ b/Zend/tests/readonly_props/cpp_reassign_visibility.phpt @@ -0,0 +1,106 @@ +--TEST-- +Promoted readonly property reassignment in constructor - visibility rules apply +--FILE-- +init(); + } + + protected function init(): void { + } +} + +class Child1 extends Parent1 { + protected function init(): void { + try { + $this->prop = 'child override'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$child1 = new Child1(); +var_dump($child1->prop); + +// Case 2: protected(set) - child's init() CAN modify within CPP window +class Parent2 { + public function __construct( + protected(set) public readonly string $prop = 'parent default', + ) { + $this->init(); + } + + protected function init(): void { + } +} + +class Child2 extends Parent2 { + protected function init(): void { + $this->prop = 'child set'; + } +} + +$child2 = new Child2(); +var_dump($child2->prop); + +// Case 3: public(set) - child's init() CAN modify within CPP window +class Parent3 { + public function __construct( + public public(set) readonly string $prop = 'parent default', + ) { + $this->init(); + } + + protected function init(): void { + } +} + +class Child3 extends Parent3 { + protected function init(): void { + $this->prop = 'child set'; + } +} + +$child3 = new Child3(); +var_dump($child3->prop); + +// Case 4: protected(set) with parent using reassignment - child cannot (window closed) +class Parent4 { + public function __construct( + protected(set) public readonly string $prop = 'parent default', + ) { + $this->prop = 'parent set'; // Uses the one reassignment + $this->init(); + } + + protected function init(): void { + } +} + +class Child4 extends Parent4 { + protected function init(): void { + try { + $this->prop = 'child override'; + } catch (Throwable $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; + } + } +} + +$child4 = new Child4(); +var_dump($child4->prop); + +?> +--EXPECT-- +Error: Cannot modify private(set) property Parent1::$prop from scope Child1 +string(14) "parent default" +string(9) "child set" +string(9) "child set" +Error: Cannot modify readonly property Parent4::$prop +string(10) "parent set" diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 37278c5cb9a23..0b18e530d4bbe 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1073,7 +1073,7 @@ static zend_never_inline zval* zend_assign_to_typed_prop(const zend_property_inf zval tmp; if (UNEXPECTED(info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { - if ((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE)) { + if ((info->flags & ZEND_ACC_READONLY) && !zend_is_readonly_property_modifiable(property_val)) { zend_readonly_property_modification_error(info); return &EG(uninitialized_zval); } @@ -5897,6 +5897,22 @@ static zend_always_inline zend_execute_data *_zend_vm_stack_push_call_frame(uint /* This callback disables optimization of "vm_stack_data" variable in VM */ ZEND_API void (ZEND_FASTCALL *zend_touch_vm_stack_data)(void *vm_stack_data) = NULL; +/* Clear IS_PROP_REINITABLE from all promoted readonly properties of the exiting + * constructor's scope. Called for both 'new Foo()' and 'parent::__construct()'. */ +static zend_always_inline void zend_ctor_clear_promoted_readonly_reinitable(zend_execute_data *ex, uint32_t call_info) +{ + if ((call_info & ZEND_CALL_HAS_THIS) && (ex->func->common.fn_flags & ZEND_ACC_CTOR)) { + zend_object *obj = Z_OBJ(ex->This); + zend_property_info *ctor_prop_info; + ZEND_HASH_MAP_FOREACH_PTR(&ex->func->common.scope->properties_info, ctor_prop_info) { + if ((ctor_prop_info->flags & (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED)) == (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED) + && IS_VALID_PROPERTY_OFFSET(ctor_prop_info->offset)) { + Z_PROP_FLAG_P(OBJ_PROP(obj, ctor_prop_info->offset)) &= ~IS_PROP_REINITABLE; + } + } ZEND_HASH_FOREACH_END(); + } +} + #include "zend_vm_execute.h" ZEND_API zend_result zend_set_user_opcode_handler(zend_uchar opcode, user_opcode_handler_t handler) diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 45eac02949d15..6b62e08810841 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1068,7 +1068,16 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva if (error) { if ((prop_info->flags & ZEND_ACC_READONLY) && Z_TYPE_P(variable_ptr) != IS_UNDEF - && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE)) { + && (!zend_is_readonly_property_modifiable(variable_ptr) + /* Also block if a foreign constructor is performing a CPP initial + * assignment on a property already initialized by the owning class's + * CPP (e.g. child redefines parent's promoted property with its own + * CPP: parent::__construct() must not consume the child's REINITABLE). */ + || ((prop_info->flags & ZEND_ACC_PROMOTED) + && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT) + && EG(current_execute_data) + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_CTOR) + && EG(current_execute_data)->func->common.scope != prop_info->ce))) { zend_readonly_property_modification_error(prop_info); variable_ptr = &EG(error_zval); goto exit; @@ -1102,7 +1111,42 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva variable_ptr = &EG(error_zval); goto exit; } - Z_PROP_FLAG_P(variable_ptr) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE); + /* For readonly properties initialized for the first time via CPP, set + * IS_PROP_REINITABLE to allow one reassignment in the constructor body. + * The flag is cleared by zend_leave_helper when the constructor exits. + * + * Classical case: the property is promoted in the declaring class and the + * executing constructor belongs to that class (scope == prop_info->ce). + * + * Extended case: a child class redeclared the property without CPP, so + * prop_info->ce is the child but the property isn't promoted there. CPP + * "ownership" still belongs to the ancestor whose constructor has CPP for + * this property name, so its body is allowed to reassign once. The clearing + * loop in zend_leave_helper iterates the exiting ctor's own promoted props, + * which share the same object slot, so cleanup happens automatically. */ + bool reinitable = false; + if ((prop_info->flags & ZEND_ACC_READONLY) + && (Z_PROP_FLAG_P(variable_ptr) & IS_PROP_UNINIT) + && EG(current_execute_data) + && (EG(current_execute_data)->func->common.fn_flags & ZEND_ACC_CTOR)) { + zend_class_entry *ctor_scope = EG(current_execute_data)->func->common.scope; + if (prop_info->flags & ZEND_ACC_PROMOTED) { + reinitable = (ctor_scope == prop_info->ce); + } else if (ctor_scope != prop_info->ce) { + /* Child redeclared without CPP: check if the executing ctor's class + * has a CPP declaration for this property name. */ + zend_property_info *scope_prop = zend_hash_find_ptr( + &ctor_scope->properties_info, prop_info->name); + reinitable = scope_prop != NULL + && (scope_prop->flags & (ZEND_ACC_READONLY|ZEND_ACC_PROMOTED)) + == (ZEND_ACC_READONLY|ZEND_ACC_PROMOTED); + } + } + if (reinitable) { + Z_PROP_FLAG_P(variable_ptr) = IS_PROP_REINITABLE; + } else { + Z_PROP_FLAG_P(variable_ptr) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE); + } value = &tmp; } diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h index 3e922343eb15a..c59ff27958e9d 100644 --- a/Zend/zend_object_handlers.h +++ b/Zend/zend_object_handlers.h @@ -277,6 +277,12 @@ ZEND_API HashTable *rebuild_object_properties_internal(zend_object *zobj); ZEND_API ZEND_COLD zend_never_inline void zend_bad_method_call(const zend_function *fbc, const zend_string *method_name, const zend_class_entry *scope); ZEND_API ZEND_COLD zend_never_inline void zend_abstract_method_call(const zend_function *fbc); +/* Check if a readonly property can be modified (has REINITABLE flag set by clone or CPP initialization) */ +static zend_always_inline bool zend_is_readonly_property_modifiable(const zval *property_val) +{ + return (Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE) != 0; +} + static zend_always_inline HashTable *zend_std_get_properties_ex(zend_object *object) { if (UNEXPECTED(zend_lazy_object_must_init(object))) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 86708f8c97a29..fe3cddd09c692 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3000,6 +3000,7 @@ ZEND_VM_HOT_HELPER(zend_leave_helper, ANY, ANY) #ifdef ZEND_PREFER_RELOAD call_info = EX_CALL_INFO(); #endif + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -3034,6 +3035,7 @@ ZEND_VM_HOT_HELPER(zend_leave_helper, ANY, ANY) * as that may free the op_array. */ zend_vm_stack_free_extra_args_ex(call_info, execute_data); + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cbfae90802cfa..abba8fe3f60de 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1160,6 +1160,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV #ifdef ZEND_PREFER_RELOAD call_info = EX_CALL_INFO(); #endif + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -1194,6 +1195,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV * as that may free the op_array. */ zend_vm_stack_free_extra_args_ex(call_info, execute_data); + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -53880,6 +53882,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend #ifdef ZEND_PREFER_RELOAD call_info = EX_CALL_INFO(); #endif + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -53914,6 +53917,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend * as that may free the op_array. */ zend_vm_stack_free_extra_args_ex(call_info, execute_data); + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -110148,6 +110152,7 @@ ZEND_API void execute_ex(zend_execute_data *ex) #ifdef ZEND_PREFER_RELOAD call_info = EX_CALL_INFO(); #endif + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { @@ -110182,6 +110187,7 @@ ZEND_API void execute_ex(zend_execute_data *ex) * as that may free the op_array. */ zend_vm_stack_free_extra_args_ex(call_info, execute_data); + zend_ctor_clear_promoted_readonly_reinitable(execute_data, call_info); if (UNEXPECTED(call_info & ZEND_CALL_RELEASE_THIS)) { OBJ_RELEASE(Z_OBJ(execute_data->This)); } else if (UNEXPECTED(call_info & ZEND_CALL_CLOSURE)) { diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 59bb9401d9a98..4b35e4c97c605 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -2806,7 +2806,7 @@ static void ZEND_FASTCALL zend_jit_assign_obj_helper(zend_object *zobj, zend_str static zend_always_inline bool verify_readonly_and_avis(zval *property_val, zend_property_info *info, bool indirect) { if (UNEXPECTED(info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK))) { - if ((info->flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(property_val) & IS_PROP_REINITABLE)) { + if ((info->flags & ZEND_ACC_READONLY) && !zend_is_readonly_property_modifiable(property_val)) { zend_readonly_property_modification_error(info); return false; }