Skip to content

[RFC] Add #[\DelayedTargetValidation] attribute #18817

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

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

DanielEScherzer
Copy link
Member

@DanielEScherzer DanielEScherzer commented Jun 9, 2025

@DanielEScherzer DanielEScherzer force-pushed the delayed-target-validation branch from 037775b to 26ad0f2 Compare July 6, 2025 17:30
@DanielEScherzer DanielEScherzer force-pushed the delayed-target-validation branch from 1978ce1 to 82f556a Compare August 1, 2025 14:38
@DanielEScherzer DanielEScherzer changed the title Add #[\DelayedTargetValidation] attribute [RFC] Add #[\DelayedTargetValidation] attribute Aug 1, 2025
@DanielEScherzer DanielEScherzer marked this pull request as ready for review August 1, 2025 15:43
@DanielEScherzer
Copy link
Member Author

CC @TimWolla @edorian for the NoDiscard-related changes, the "don't apply on property hooks" error was moved around so that it could be delayed

@TimWolla
Copy link
Member

TimWolla commented Aug 1, 2025

Currently on vacation, so can't review, but I trust that you are capable of correctly moving error messages 😄

Copy link
Member

@TimWolla TimWolla left a comment

Choose a reason for hiding this comment

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

Some first thoughts. I don't currently have the mental capacity to perform an in-depth review (diff is larger than anticipated) and didn't look at the entire PR.

Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

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

After sleeping on this, my preferred approach would be:

  • Stop using zend_error_noreturn() in validator in favor of zend_throw_error() for target validation, instruct from UPGRADING.INTERNALS to do the same for extensions.
  • Use EG(exception) from zend_compile_attributes() to understand whether the validator succeeded.
  • Without #[DelayedTargetValidation], promote to a fatal error with zend_exception_uncaught_error().
  • With #[DelayedTargetValidation], record whether the validator has succeeded and store it in zend_attribute. EG(exception) would have to be removed/freed.
  • When it failed, repeat the validation on ReflectionAttribute::newInstance(). Skipping for successful validators prevents writing to locked shm. Validators should be idempotent, i.e. we should be able to rely on the fact that calling it again will also throw.

The benefit is that we don't need to handle this for every validator, and we don't need the ZEND_ATTRIBUTE_NO_TARGET_VALIDATION/ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION flags. Then, this feature should work mostly automatically.

WDYT?

@@ -8422,6 +8446,10 @@ static zend_op_array *zend_compile_func_decl_ex(

CG(active_op_array) = op_array;

zend_oparray_context_begin(&orig_oparray_context, op_array);
CG(context).active_property_info_name = property_info_name;
CG(context).active_property_hook_kind = hook_kind;
Copy link
Member

Choose a reason for hiding this comment

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

Same with this.

@DanielEScherzer
Copy link
Member Author

Stop using zend_error_noreturn() in validator in favor of zend_throw_error() for target validation, instruct from UPGRADING.INTERNALS to do the same for extensions.

zend_throw_error() will just use zend_error_noreturn() if I'm reading correctly,

php-src/Zend/zend.c

Lines 1831 to 1836 in 222f751

//TODO: we can't convert compile-time errors to exceptions yet???
if (EG(current_execute_data) && !CG(in_compilation)) {
zend_throw_exception(exception_ce, message, 0);
} else {
zend_error_noreturn(E_ERROR, "%s", message);
}

when validators are run, CG(in_compilation) is true, see the validate_nodiscard() logic

Skipping for successful validators prevents writing to locked shm.

No validators should be writing anything at runtime, e.g. for allow dynamic properties the validator just ZEND_ASSERTs that it passed the first time

@iluuu1994
Copy link
Member

zend_throw_error() will just use zend_error_noreturn() if I'm reading correctly,

RIght, at least when EG(current_execute_data) is empty, which can occur for the main script. You would have to install a fake frame.

No validators should be writing anything at runtime, e.g. for allow dynamic properties the validator just ZEND_ASSERTs that it passed the first time

Yes, but every validator must abort to avoid this, which is a potential foot gun. What I'm suggesting is a way for #[DelayTargetValidation] to mostly just work.

@DanielEScherzer
Copy link
Member Author

zend_throw_error() will just use zend_error_noreturn() if I'm reading correctly,

RIght, at least when EG(current_execute_data) is empty, which can occur for the main script. You would have to install a fake frame.

Even if it is not empty, the && means that noreturn would be used? Or am I missing something?

@iluuu1994
Copy link
Member

Yes, you can change this value as well. But there are alternatives. E.g. validator could return a zend_string containing the error message or null on success, which could be attached to zend_attribute and then re-thrown by reflection. Then you don't need to trigger validator twice at all.

@DanielEScherzer
Copy link
Member Author

I switched it so that validators return error messages instead of emitting them, which allowed removing the ZEND_ATTRIBUTE_NO_TARGET_VALIDATION flag. For some reason I kept getting segfaults if I tried to add a field to the zend_attribute struct to hold the error message, even if I didn't actually touch the field, I'll try to debug

@iluuu1994
Copy link
Member

Valgrind or sanitizers might help. Adding -m to run-tests.php is a simple way to run Valgrind on some tests without having to recompile with sanitizers.

@DanielEScherzer
Copy link
Member Author

Valgrind or sanitizers might help. Adding -m to run-tests.php is a simple way to run Valgrind on some tests without having to recompile with sanitizers.

Pretty sure the issue was that I was putting the new field after the zend_attribute_arg args[1]; and so breaking the struct hack

@DanielEScherzer DanielEScherzer force-pushed the delayed-target-validation branch from 8959bec to 254926f Compare August 15, 2025 20:45
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

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

Thanks for the changes thus far. It seems simpler to me, hopefully you agree.


/* Report the delayed validator error for internal attributes */
if (delayed_target_validation && ce->type == ZEND_INTERNAL_CLASS) {
Copy link
Member

Choose a reason for hiding this comment

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

Don't think we need to load this at all. When validation_error is set, we can just throw the error.

* - the attribute is a user attribute, and neither target nor repetition
* have been validated.
*/
uint32_t flags = zend_attribute_attribute_get_flags(marker, ce);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of these changes, could we not just attach target & repetition validation to validation_error in zend_compile.c? Then errors would all be handled the same way for internal attributes.

Comment on lines +7567 to +7568
"delayedtargetvalidation",
strlen("delayedtargetvalidation"),
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
"delayedtargetvalidation",
strlen("delayedtargetvalidation"),
ZEND_STRL("delayedtargetvalidation"),

{
ZEND_ASSERT(CG(in_compilation));
const zend_string *prop_info_name = CG(context).active_property_info_name;
if (prop_info_name == NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: No hard rules on this, but most similar code puts the happy path at the bottom. This way, more conditions can be added without nesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants