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

Merged
merged 23 commits into from
Aug 20, 2025

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.

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.

C stuff LGTM now, didn't look into the (very verbose) tests.

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!

@DanielEScherzer DanielEScherzer force-pushed the delayed-target-validation branch from 9152360 to 3dac774 Compare August 17, 2025 21:37
@DanielEScherzer
Copy link
Member Author

Addressed the notes about comments, and rebased
I'll plan to merge this tomorrow, I want to take another look myself with fresh eyes
In the meantime I'll go write up a blog post explaining the feature that can be used to help with the process of updating php/doc-en later

@DanielEScherzer DanielEScherzer merged commit 63acc4b into php:master Aug 20, 2025
9 checks passed
@DanielEScherzer DanielEScherzer deleted the delayed-target-validation branch August 20, 2025 07:41
);
ZEND_ASSERT(delayed_target_validation != NULL);
#endif
zend_throw_exception(zend_ce_error, ZSTR_VAL(attr->data->validation_error), 0);
Copy link
Member

Choose a reason for hiding this comment

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

Seeing this, I wonder if we should export zend_throw_exception_zstr.

Copy link
Member Author

Choose a reason for hiding this comment

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

Don't think that is necessary at the moment, this worked fine

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