@@ -1111,16 +1111,38 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
11111111 variable_ptr = & EG (error_zval );
11121112 goto exit ;
11131113 }
1114- /* For promoted readonly properties being initialized for the first time,
1115- * set IS_PROP_REINITABLE to allow one reassignment in the constructor.
1116- * The flag will be cleared by zend_leave_helper when the constructor exits.
1117- * We check the current execute data directly (no stack walk needed) because
1118- * CPP initialization always runs within the constructor frame itself. */
1119- if ((prop_info -> flags & (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED )) == (ZEND_ACC_READONLY | ZEND_ACC_PROMOTED )
1114+ /* For readonly properties initialized for the first time via CPP, set
1115+ * IS_PROP_REINITABLE to allow one reassignment in the constructor body.
1116+ * The flag is cleared by zend_leave_helper when the constructor exits.
1117+ *
1118+ * Classical case: the property is promoted in the declaring class and the
1119+ * executing constructor belongs to that class (scope == prop_info->ce).
1120+ *
1121+ * Extended case: a child class redeclared the property without CPP, so
1122+ * prop_info->ce is the child but the property isn't promoted there. CPP
1123+ * "ownership" still belongs to the ancestor whose constructor has CPP for
1124+ * this property name, so its body is allowed to reassign once. The clearing
1125+ * loop in zend_leave_helper iterates the exiting ctor's own promoted props,
1126+ * which share the same object slot, so cleanup happens automatically. */
1127+ bool reinitable = false;
1128+ if ((prop_info -> flags & ZEND_ACC_READONLY )
11201129 && (Z_PROP_FLAG_P (variable_ptr ) & IS_PROP_UNINIT )
11211130 && EG (current_execute_data )
1122- && (EG (current_execute_data )-> func -> common .fn_flags & ZEND_ACC_CTOR )
1123- && EG (current_execute_data )-> func -> common .scope == prop_info -> ce ) {
1131+ && (EG (current_execute_data )-> func -> common .fn_flags & ZEND_ACC_CTOR )) {
1132+ zend_class_entry * ctor_scope = EG (current_execute_data )-> func -> common .scope ;
1133+ if (prop_info -> flags & ZEND_ACC_PROMOTED ) {
1134+ reinitable = (ctor_scope == prop_info -> ce );
1135+ } else if (ctor_scope != prop_info -> ce ) {
1136+ /* Child redeclared without CPP: check if the executing ctor's class
1137+ * has a CPP declaration for this property name. */
1138+ zend_property_info * scope_prop = zend_hash_find_ptr (
1139+ & ctor_scope -> properties_info , prop_info -> name );
1140+ reinitable = scope_prop != NULL
1141+ && (scope_prop -> flags & (ZEND_ACC_READONLY |ZEND_ACC_PROMOTED ))
1142+ == (ZEND_ACC_READONLY |ZEND_ACC_PROMOTED );
1143+ }
1144+ }
1145+ if (reinitable ) {
11241146 Z_PROP_FLAG_P (variable_ptr ) = IS_PROP_REINITABLE ;
11251147 } else {
11261148 Z_PROP_FLAG_P (variable_ptr ) &= ~(IS_PROP_UNINIT |IS_PROP_REINITABLE );
0 commit comments