@@ -48,7 +48,11 @@ HashTable* pmmpthread_read_debug(PMMPTHREAD_READ_DEBUG_PASSTHRU_D) {
4848HashTable * pmmpthread_read_properties (PMMPTHREAD_READ_PROPERTIES_PASSTHRU_D ) {
4949 pmmpthread_zend_object_t * threaded = PMMPTHREAD_FETCH_FROM (object );
5050
51+ #if PHP_VERSION_ID >= 80400
52+ zend_std_get_properties_ex (& threaded -> std );
53+ #else
5154 rebuild_object_properties (& threaded -> std );
55+ #endif
5256
5357 pmmpthread_store_tohash (
5458 & threaded -> std , threaded -> std .properties );
@@ -75,6 +79,81 @@ zval* pmmpthread_read_dimension(PMMPTHREAD_READ_DIMENSION_PASSTHRU_D) {
7579 return rv ;
7680}
7781
82+ #if PHP_VERSION_ID >= 80400
83+ /* {{{
84+ * Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
85+ */
86+ static bool zend_is_in_hook (const zend_property_info * prop_info )
87+ {
88+ zend_execute_data * execute_data = EG (current_execute_data );
89+ if (!execute_data || !EX (func ) || !EX (func )-> common .prop_info ) {
90+ return false;
91+ }
92+
93+ const zend_property_info * parent_info = EX (func )-> common .prop_info ;
94+ ZEND_ASSERT (prop_info -> prototype && parent_info -> prototype );
95+ return prop_info -> prototype == parent_info -> prototype ;
96+ } /* }}} */
97+
98+ /* {{{
99+ * Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
100+ */
101+ static bool zend_should_call_hook (const zend_property_info * prop_info , const zend_object * obj )
102+ {
103+ if (!zend_is_in_hook (prop_info )) {
104+ return true;
105+ }
106+
107+ /* execute_data and This are guaranteed to be set if zend_is_in_hook() returns true. */
108+ zend_object * parent_obj = Z_OBJ (EG (current_execute_data )-> This );
109+ if (parent_obj == obj ) {
110+ return false;
111+ }
112+
113+ /* pmmpthread objects can't be lazy */
114+ /*
115+ if (zend_object_is_lazy_proxy(parent_obj)
116+ && zend_lazy_object_initialized(parent_obj)
117+ && zend_lazy_object_get_instance(parent_obj) == obj) {
118+ return false;
119+ }
120+ */
121+
122+ return true;
123+ } /* }}} */
124+
125+ /* {{{
126+ * Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
127+ */
128+ static void zend_throw_no_prop_backing_value_access (zend_string * class_name , zend_string * prop_name , bool is_read )
129+ {
130+ zend_throw_error (NULL , "Must not %s virtual property %s::$%s" ,
131+ is_read ? "read from" : "write to" ,
132+ ZSTR_VAL (class_name ), ZSTR_VAL (prop_name ));
133+ } /* }}} */
134+
135+ /* {{{
136+ * Copy pasta from zend_object_handlers.c because the equivalent over there had to be static...
137+ */
138+ static bool zend_call_get_hook (
139+ const zend_property_info * prop_info , zend_string * prop_name ,
140+ zend_function * get , zend_object * zobj , zval * rv )
141+ {
142+ if (!zend_should_call_hook (prop_info , zobj )) {
143+ if (UNEXPECTED (prop_info -> flags & ZEND_ACC_VIRTUAL )) {
144+ zend_throw_no_prop_backing_value_access (zobj -> ce -> name , prop_name , /* is_read */ true);
145+ }
146+ return false;
147+ }
148+
149+ GC_ADDREF (zobj );
150+ zend_call_known_instance_method_with_0_params (get , zobj , rv );
151+ OBJ_RELEASE (zobj );
152+
153+ return true;
154+ }
155+ #endif //PHP_VERSION_ID >= 80400
156+
78157zval * pmmpthread_read_property (PMMPTHREAD_READ_PROPERTY_PASSTHRU_D ) {
79158 zval zmember ;
80159 zend_guard * guard ;
@@ -87,9 +166,40 @@ zval* pmmpthread_read_property(PMMPTHREAD_READ_PROPERTY_PASSTHRU_D) {
87166 (* guard ) &= ~IN_GET ;
88167 } else {
89168 zend_property_info * info = zend_get_property_info (object -> ce , member , 0 );
169+
90170 if (info == ZEND_WRONG_PROPERTY_INFO ) {
91- rv = & EG (uninitialized_zval );
92- } else if (info == NULL || (info -> flags & ZEND_ACC_STATIC ) != 0 ) { //dynamic property
171+ return & EG (uninitialized_zval );
172+ }
173+
174+ if (info != NULL ) {
175+ if (info -> flags & ZEND_ACC_STATIC ) {
176+ info = NULL ;
177+ #if PHP_VERSION_ID >= 80400
178+ } else if (info -> hooks != NULL ) {
179+ zend_function * get = info -> hooks [ZEND_PROPERTY_HOOK_GET ];
180+ if (!get ) {
181+ if (info -> flags & ZEND_ACC_VIRTUAL ) {
182+ zend_throw_error (NULL , "Property %s::$%s is write-only" ,
183+ ZSTR_VAL (object -> ce -> name ), ZSTR_VAL (member ));
184+ return & EG (uninitialized_zval );
185+ }
186+ //php-src has some checks for indirection here - we don't need these
187+ //since we don't allow indirect modification on ThreadSafe objects anyway
188+
189+ //fallthru to pmmpthread_store_read() below
190+ } else {
191+ if (zend_call_get_hook (info , member , get , object , rv )) {
192+ return rv ;
193+ }
194+ if (EG (exception )) {
195+ return & EG (uninitialized_zval );
196+ }
197+ }
198+ #endif
199+ }
200+ }
201+
202+ if (info == NULL ) { //dynamic property
93203 if (pmmpthread_store_read (object , & zmember , type , rv ) == FAILURE ) {
94204 if (type != BP_VAR_IS ) {
95205 zend_error (E_WARNING , "Undefined property: %s::$%s" , ZSTR_VAL (object -> ce -> name ), ZSTR_VAL (member ));
@@ -137,11 +247,11 @@ void pmmpthread_write_dimension(PMMPTHREAD_WRITE_DIMENSION_PASSTHRU_D) {
137247
138248zval * pmmpthread_write_property (PMMPTHREAD_WRITE_PROPERTY_PASSTHRU_D ) {
139249 zval zmember ;
140- zval tmp ;
250+ zval coerced_value ;
141251 zend_guard * guard ;
142252
143253 ZVAL_STR (& zmember , member );
144- ZVAL_UNDEF (& tmp );
254+ ZVAL_UNDEF (& coerced_value );
145255
146256 if (object -> ce -> __set && (guard = zend_get_property_guard (object , member )) && !((* guard ) & IN_SET )) {
147257 zval rv ;
@@ -154,42 +264,83 @@ zval* pmmpthread_write_property(PMMPTHREAD_WRITE_PROPERTY_PASSTHRU_D) {
154264 if (Z_TYPE (rv ) != IS_UNDEF )
155265 zval_dtor (& rv );
156266 } else {
157- bool ok = true;
267+ bool write_store = true;
158268 zend_property_info * info = zend_get_property_info (object -> ce , member , 0 );
159- if (info ! = ZEND_WRONG_PROPERTY_INFO ) {
160- if ( info != NULL && ( info -> flags & ZEND_ACC_STATIC ) == 0 ) {
161- ZVAL_STR ( & zmember , info -> name ); //use mangled name to avoid private member shadowing issues
269+ if (info = = ZEND_WRONG_PROPERTY_INFO ) {
270+ return & EG ( error_zval );
271+ }
162272
273+ //zend_verify_property_type() might modify the value
274+ //value is not copied before we receive it, so it might be
275+ //from opcache protected memory which we can't modify
276+ ZVAL_COPY (& coerced_value , value );
277+
278+ if (info != NULL && (info -> flags & ZEND_ACC_STATIC ) == 0 ) {
279+ ZVAL_STR (& zmember , info -> name ); //use mangled name to avoid private member shadowing issues
280+
281+ #if PHP_VERSION_ID >= 80400
282+ if (info -> hooks != NULL ) {
283+ zend_function * set = info -> hooks [ZEND_PROPERTY_HOOK_SET ];
284+
285+ if (!set ) {
286+ if (info -> flags & ZEND_ACC_VIRTUAL ) {
287+ zend_throw_error (NULL , "Property %s::$%s is read-only" , ZSTR_VAL (object -> ce -> name ), ZSTR_VAL (member ));
288+ value = & EG (error_zval );
289+ write_store = false;
290+ }
291+ //fallthru to normal write
292+ } else if (!zend_should_call_hook (info , object )) {
293+ if (info -> flags & ZEND_ACC_VIRTUAL ) {
294+ zend_throw_no_prop_backing_value_access (object -> ce -> name , member , /* is_read */ false);
295+ value = & EG (error_zval );
296+ write_store = false;
297+ }
298+ //fallthru to normal write
299+ } else {
300+ //call hook or die trying
301+ write_store = false;
302+ if (UNEXPECTED (info -> flags & ZEND_ACC_PPP_SET_MASK
303+ && !zend_asymmetric_property_has_set_access (info ))) {
304+ zend_asymmetric_visibility_property_modification_error (info , "modify" );
305+ value = & EG (error_zval );
306+ } else {
307+ GC_ADDREF (object );
308+ zend_call_known_instance_method_with_1_params (set , object , NULL , value );
309+ OBJ_RELEASE (object );
310+ }
311+ }
312+ }
313+ #endif
314+ if (write_store ) { //if hooked, the setter will do the type verification, so we can skip this
163315 zend_execute_data * execute_data = EG (current_execute_data );
164316 bool strict = execute_data
165317 && execute_data -> func
166318 && ZEND_CALL_USES_STRICT_TYPES (EG (current_execute_data ));
167319
168- //zend_verify_property_type() might modify the value
169- //value is not copied before we receive it, so it might be
170- //from opcache protected memory which we can't modify
171- ZVAL_COPY (& tmp , value );
172- value = & tmp ;
173-
174- if (ZEND_TYPE_IS_SET (info -> type ) && !zend_verify_property_type (info , value , strict )) {
175- ok = false;
320+ if (ZEND_TYPE_IS_SET (info -> type ) && !zend_verify_property_type (info , & coerced_value , strict )) {
321+ write_store = false;
176322 }
177323 }
324+ }
178325
179- if (ok && pmmpthread_store_write (object , & zmember , value , PMMPTHREAD_STORE_NO_COERCE_ARRAY ) == FAILURE && !EG (exception )) {
180- zend_throw_error (
181- pmmpthread_ce_nts_value_error ,
182- "Cannot assign non-thread-safe value of type %s to thread-safe class property %s::$%s" ,
183- zend_zval_type_name (value ),
184- ZSTR_VAL (object -> ce -> name ),
185- ZSTR_VAL (member )
186- );
187- }
326+ if (write_store && pmmpthread_store_write (object , & zmember , & coerced_value , PMMPTHREAD_STORE_NO_COERCE_ARRAY ) == FAILURE && !EG (exception )) {
327+ zend_throw_error (
328+ pmmpthread_ce_nts_value_error ,
329+ "Cannot assign non-thread-safe value of type %s to thread-safe class property %s::$%s" ,
330+ zend_zval_type_name (value ),
331+ ZSTR_VAL (object -> ce -> name ),
332+ ZSTR_VAL (member )
333+ );
188334 }
189335 }
190336
191- zval_ptr_dtor (& tmp );
337+ zval_ptr_dtor (& coerced_value );
192338
339+ //technically, this should return the coercedValue, but PHP doesn't give us a way to do that
340+ //and we can't return a ptr to a stack variable
341+ //so instead this will mirror the behaviour of a hooked property and return the original, non-coerced value
342+ //this is an unfortunate deviation from the standard behaviour, but since we should only see coercion in
343+ //weak mode anyway, it shouldn't cause anyone any serious headaches.
193344 return EG (exception ) ? & EG (error_zval ) : value ;
194345}
195346/* }}} */
@@ -222,16 +373,55 @@ int pmmpthread_has_property(PMMPTHREAD_HAS_PROPERTY_PASSTHRU_D) {
222373 (* guard ) &= ~IN_ISSET ;
223374
224375 if (Z_TYPE (rv ) != IS_UNDEF ) {
376+ //TODO: this doesn't account for ZEND_PROPERTY_NOT_EMPTY
225377 isset = zend_is_true (& rv );
226378 zval_dtor (& rv );
227379 }
228380 } else {
229381 zend_property_info * info = zend_get_property_info (object -> ce , member , 1 );
382+ bool read_store = true;
230383 if (info != ZEND_WRONG_PROPERTY_INFO ) {
231384 if (info != NULL && (info -> flags & ZEND_ACC_STATIC ) == 0 ) {
232- ZVAL_STR (& zmember , info -> name ); //defined property, use mangled name
385+ ZVAL_STR (& zmember , info -> name );
386+ #if PHP_VERSION_ID >= 80400
387+ if (info -> hooks != NULL ) {
388+ zend_function * get = info -> hooks [ZEND_PROPERTY_HOOK_GET ];
389+
390+ if (has_set_exists == ZEND_PROPERTY_EXISTS ) {
391+ isset = 1 ;
392+ read_store = false;
393+ } else if (get == NULL ) {
394+ if (info -> flags & ZEND_ACC_VIRTUAL ) {
395+ zend_throw_error (NULL , "Property %s::$%s is write-only" ,
396+ ZSTR_VAL (object -> ce -> name ), ZSTR_VAL (member ));
397+ isset = 0 ;
398+ read_store = false;
399+ }
400+
401+ //fallthru to store read
402+ } else {
403+ zval rv ;
404+ if (zend_call_get_hook (info , member , get , object , & rv )) {
405+ read_store = false;
406+ if (has_set_exists == ZEND_PROPERTY_NOT_EMPTY ) {
407+ isset = zend_is_true (& rv );
408+ } else {
409+ ZEND_ASSERT (has_set_exists == ZEND_PROPERTY_ISSET );
410+ isset = Z_TYPE (rv ) != IS_NULL
411+ && (Z_TYPE (rv ) != IS_REFERENCE || Z_TYPE_P (Z_REFVAL (rv )) != IS_NULL );
412+ }
413+ zval_ptr_dtor (& rv );
414+ } else if (EG (exception )) {
415+ read_store = false;
416+ }
417+ }
418+ }
419+ #endif
420+ }
421+
422+ if (read_store ) {
423+ isset = pmmpthread_store_isset (object , & zmember , has_set_exists );
233424 }
234- isset = pmmpthread_store_isset (object , & zmember , has_set_exists );
235425 } else isset = 0 ;
236426 }
237427 return isset ;
@@ -270,6 +460,13 @@ void pmmpthread_unset_property(PMMPTHREAD_UNSET_PROPERTY_PASSTHRU_D) {
270460 if (info != ZEND_WRONG_PROPERTY_INFO ) {
271461 if (info != NULL && (info -> flags & ZEND_ACC_STATIC ) == 0 ) {
272462 ZVAL_STR (& zmember , info -> name ); //defined property, use mangled name
463+ #if PHP_VERSION_ID >= 80400
464+ if (info -> hooks != NULL ) {
465+ zend_throw_error (NULL , "Cannot unset hooked property %s::$%s" ,
466+ ZSTR_VAL (object -> ce -> name ), ZSTR_VAL (member ));
467+ return ;
468+ }
469+ #endif
273470 }
274471 pmmpthread_store_delete (object , & zmember );
275472 }
0 commit comments