From 52b56eb2543c9acb9a6ef5169b5fcda098acb833 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Sat, 4 Jul 2026 01:32:47 +0800 Subject: [PATCH 1/5] ext/Intl: Fix IntlDateFormatter offset type validation (#22533) IntlDateFormatter::parse()/datefmt_parse() and IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError when the offset argument is not of type int instead of silently converting the value. --- NEWS | 3 ++ UPGRADING | 4 ++ ext/intl/dateformat/dateformat_parse.cpp | 18 +++++--- ...fmt_parse_localtime_offset_type_error.phpt | 44 +++++++++++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 ext/intl/tests/datefmt_parse_localtime_offset_type_error.phpt diff --git a/NEWS b/NEWS index caddbd717490..216f3b2b52b9 100644 --- a/NEWS +++ b/NEWS @@ -132,6 +132,9 @@ PHP NEWS locale_get_display_keyword_value() respectively. (Weilin Du) . Fix incorrect argument positions for invalid start/end arguments in transliterator_transliterate(). (Weilin Du) + . IntlDateFormatter::parse()/datefmt_parse() and + IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError + when the offset argument is not of type int. (Weilin Du) . Fixed IntlTimeZone::getDisplayName() to synchronize object error state for invalid display types. (Weilin Du) . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the diff --git a/UPGRADING b/UPGRADING index eb6d182c54a7..a42d5ec355a6 100644 --- a/UPGRADING +++ b/UPGRADING @@ -56,6 +56,10 @@ PHP 8.6 UPGRADE NOTES . ResourceBundle::get() and resourcebundle_get() now report fallback-disabled resource lookups with "without fallback to " instead of the malformed "without fallback from to ". + . IntlDateFormatter::parse()/datefmt_parse() and + IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError + when the offset argument is not of type int instead of silently converting + the value. - PCNTL: . pcntl_alarm() now raises a ValueError if the seconds argument is diff --git a/ext/intl/dateformat/dateformat_parse.cpp b/ext/intl/dateformat/dateformat_parse.cpp index d818627439e3..0537b42feec5 100644 --- a/ext/intl/dateformat/dateformat_parse.cpp +++ b/ext/intl/dateformat/dateformat_parse.cpp @@ -147,9 +147,12 @@ U_CFUNC PHP_FUNCTION(datefmt_parse) DATE_FORMAT_METHOD_FETCH_OBJECT; if (z_parse_pos) { - zval *z_parse_pos_tmp = z_parse_pos; - ZVAL_DEREF(z_parse_pos_tmp); - const zend_long long_parse_pos = zval_get_long(z_parse_pos_tmp); + bool failed; + const zend_long long_parse_pos = zval_try_get_long(z_parse_pos, &failed); + if (failed) { + zend_argument_type_error(hasThis() ? 2 : 3, "must be of type int, %s given", zend_zval_value_name(z_parse_pos)); + RETURN_THROWS(); + } if (ZEND_LONG_INT_OVFL(long_parse_pos)) { intl_error_set_code(NULL, U_ILLEGAL_ARGUMENT_ERROR); intl_error_set_custom_msg(NULL, "String index is out of valid range."); @@ -229,9 +232,12 @@ U_CFUNC PHP_FUNCTION(datefmt_localtime) DATE_FORMAT_METHOD_FETCH_OBJECT; if (z_parse_pos) { - zval *z_parse_pos_tmp = z_parse_pos; - ZVAL_DEREF(z_parse_pos_tmp); - const zend_long long_parse_pos = zval_get_long(z_parse_pos_tmp); + bool failed; + const zend_long long_parse_pos = zval_try_get_long(z_parse_pos, &failed); + if (failed) { + zend_argument_type_error(hasThis() ? 2 : 3, "must be of type int, %s given", zend_zval_value_name(z_parse_pos)); + RETURN_THROWS(); + } if (ZEND_LONG_INT_OVFL(long_parse_pos)) { intl_error_set_code(NULL, U_ILLEGAL_ARGUMENT_ERROR); intl_error_set_custom_msg(NULL, "String index is out of valid range."); diff --git a/ext/intl/tests/datefmt_parse_localtime_offset_type_error.phpt b/ext/intl/tests/datefmt_parse_localtime_offset_type_error.phpt new file mode 100644 index 000000000000..5c3d03cd4f65 --- /dev/null +++ b/ext/intl/tests/datefmt_parse_localtime_offset_type_error.phpt @@ -0,0 +1,44 @@ +--TEST-- +datefmt_parse() and datefmt_localtime() validate offset type +--EXTENSIONS-- +intl +--FILE-- +setPattern('VV'); + +$offset = 'offset'; +try { + $fmt->parse('America/Los_Angeles', $offset); +} catch (TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +$offset = 'offset'; +try { + datefmt_parse($fmt, 'America/Los_Angeles', $offset); +} catch (TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +$offset = 'offset'; +try { + $fmt->localtime('America/Los_Angeles', $offset); +} catch (TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +$offset = 'offset'; +try { + datefmt_localtime($fmt, 'America/Los_Angeles', $offset); +} catch (TypeError $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +IntlDateFormatter::parse(): Argument #2 ($offset) must be of type int, string given +datefmt_parse(): Argument #3 ($offset) must be of type int, string given +IntlDateFormatter::localtime(): Argument #2 ($offset) must be of type int, string given +datefmt_localtime(): Argument #3 ($offset) must be of type int, string given From 4fc0819165f8dcc307ccf7f2434342a612e000ac Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Sat, 4 Jul 2026 01:52:58 +0800 Subject: [PATCH 2/5] [skip ci] Fix NEWS entry for intl fix This moves the NEWS entry of #22533 to 8.6 alpha2 --- NEWS | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 216f3b2b52b9..3ea51b89f234 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,11 @@ PHP NEWS . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size" warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) +- Intl: + . IntlDateFormatter::parse()/datefmt_parse() and + IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError + when the offset argument is not of type int. (Weilin Du) + - Opcache: . Fixed bug GH-21770 (Infinite recursion in property hook getter in opcache preloaded trait). (iliaal) @@ -132,9 +137,6 @@ PHP NEWS locale_get_display_keyword_value() respectively. (Weilin Du) . Fix incorrect argument positions for invalid start/end arguments in transliterator_transliterate(). (Weilin Du) - . IntlDateFormatter::parse()/datefmt_parse() and - IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError - when the offset argument is not of type int. (Weilin Du) . Fixed IntlTimeZone::getDisplayName() to synchronize object error state for invalid display types. (Weilin Du) . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the From dd5769440296460502b3674f72a6be8d43769ac3 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 3 Jul 2026 07:12:46 -0400 Subject: [PATCH 3/5] Fix GH-22570: stack overflow serializing a deeply nested Dom\XMLDocument The new-DOM XML serializer recurses through dom_xml_serialization_algorithm() for every element child, so a document nested deeply enough overflows the C stack and crashes during saveXml() or innerHTML. Add a stack-limit check at the dispatcher, throwing an Error on overflow, mirroring bd724bd. Gate the "Could not save document" warning and the innerHTML not-well-formed exception with !EG(exception) so the thrown Error propagates cleanly instead of being accompanied by a warning or replaced with the wrong exception type. Fixes GH-22570 Closes GH-22576 --- NEWS | 4 +++ ext/dom/document.c | 4 ++- ext/dom/inner_html_mixin.c | 4 ++- ext/dom/tests/modern/xml/gh22570.phpt | 40 +++++++++++++++++++++++++++ ext/dom/xml_serializer.c | 14 ++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 ext/dom/tests/modern/xml/gh22570.phpt diff --git a/NEWS b/NEWS index 6d4d1865d790..08a852a5a0d8 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,10 @@ PHP NEWS - DBA: . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz) +- DOM: + . Fixed bug GH-22570 (Stack overflow when serializing a deeply nested + Dom\XMLDocument). (iliaal) + - Exif: . Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size" warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) diff --git a/ext/dom/document.c b/ext/dom/document.c index e4d285c990fe..9c269e4cb14d 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -1678,7 +1678,9 @@ static void dom_document_save_xml(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry } if (!res) { - php_error_docref(NULL, E_WARNING, "Could not save document"); + if (!EG(exception)) { + php_error_docref(NULL, E_WARNING, "Could not save document"); + } RETURN_FALSE; } else { RETURN_NEW_STR(res); diff --git a/ext/dom/inner_html_mixin.c b/ext/dom/inner_html_mixin.c index 19f640e6b4ac..e81ca5a7b238 100644 --- a/ext/dom/inner_html_mixin.c +++ b/ext/dom/inner_html_mixin.c @@ -103,7 +103,9 @@ zend_result dom_element_inner_html_read(dom_object *obj, zval *retval) } if (UNEXPECTED(status < 0)) { smart_str_free_ex(&str, false); - php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true); + if (!EG(exception)) { + php_dom_throw_error_with_message(SYNTAX_ERR, "The resulting XML serialization is not well-formed", true); + } return FAILURE; } ZVAL_STR(retval, smart_str_extract(&str)); diff --git a/ext/dom/tests/modern/xml/gh22570.phpt b/ext/dom/tests/modern/xml/gh22570.phpt new file mode 100644 index 000000000000..af78acf00dfa --- /dev/null +++ b/ext/dom/tests/modern/xml/gh22570.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-22570 (Stack overflow when serializing a deeply nested Dom\XMLDocument) +--EXTENSIONS-- +dom +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=512K +--FILE-- +appendChild($doc->createElement('root')); +for ($i = 0; $i < 100000; $i++) { + $node = $node->appendChild($doc->createElement('a')); +} + +try { + $doc->saveXml(); +} catch (\Error $e) { + echo "saveXml: ", $e::class, ": ", $e->getMessage(), "\n"; +} + +try { + $doc->documentElement->innerHTML; +} catch (\Error $e) { + echo "innerHTML: ", $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +saveXml: Error: Maximum call stack size reached. Infinite recursion? +innerHTML: Error: Maximum call stack size reached. Infinite recursion? diff --git a/ext/dom/xml_serializer.c b/ext/dom/xml_serializer.c index 5e6a6f93b91c..2d9e55a7b706 100644 --- a/ext/dom/xml_serializer.c +++ b/ext/dom/xml_serializer.c @@ -1250,6 +1250,15 @@ static int dom_xml_serializing_a_document_node( return 0; } +static zend_always_inline bool dom_xml_serialize_check_stack_limit(void) +{ +#ifdef ZEND_CHECK_STACK_LIMIT + return zend_call_stack_overflowed(EG(stack_limit)); +#else + return false; +#endif +} + /* https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization-algorithm */ static int dom_xml_serialization_algorithm( dom_xml_serialize_ctx *ctx, @@ -1261,6 +1270,11 @@ static int dom_xml_serialization_algorithm( bool require_well_formed ) { + if (UNEXPECTED(dom_xml_serialize_check_stack_limit())) { + zend_throw_error(NULL, "Maximum call stack size reached. Infinite recursion?"); + return -1; + } + /* If node's interface is: */ switch (node->type) { case XML_ELEMENT_NODE: From 154881aa4ecaded7bee07b07debf108da061e8a3 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Sat, 4 Jul 2026 02:03:08 +0800 Subject: [PATCH 4/5] ext/intl: Fix NumberFormatter parse offset overflow (#22572) Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to reject offset values outside the 32-bit range instead of silently truncating them. --- NEWS | 3 ++ ext/intl/formatter/formatter_parse.cpp | 16 +++++-- .../formatter_parse_offset_overflow.phpt | 46 +++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 ext/intl/tests/formatter_parse_offset_overflow.phpt diff --git a/NEWS b/NEWS index 3ea51b89f234..61a49cc1750a 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ PHP NEWS warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman) - Intl: + . Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to + reject offset values outside the 32-bit range instead of silently + truncating them. (Weilin Du) . IntlDateFormatter::parse()/datefmt_parse() and IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError when the offset argument is not of type int. (Weilin Du) diff --git a/ext/intl/formatter/formatter_parse.cpp b/ext/intl/formatter/formatter_parse.cpp index a475960809b8..63c0fba4329f 100644 --- a/ext/intl/formatter/formatter_parse.cpp +++ b/ext/intl/formatter/formatter_parse.cpp @@ -50,7 +50,12 @@ U_CFUNC PHP_FUNCTION( numfmt_parse ) } if (zposition) { - position = (int32_t) zval_get_long(zposition); + zend_long long_position = zval_get_long(zposition); + if (long_position < INT32_MIN || long_position > INT32_MAX) { + zend_argument_value_error(hasThis() ? 3 : 4, "must be between %d and %d", INT32_MIN, INT32_MAX); + RETURN_THROWS(); + } + position = (int32_t) long_position; } /* Fetch the object. */ @@ -155,8 +160,13 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency ) intl_stringFromChar(ustr, str, str_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); - if(zposition) { - position = (int32_t) zval_get_long(zposition); + if (zposition) { + zend_long long_position = zval_get_long(zposition); + if (long_position < INT32_MIN || long_position > INT32_MAX) { + zend_argument_value_error(hasThis() ? 3 : 4, "must be between %d and %d", INT32_MIN, INT32_MAX); + RETURN_THROWS(); + } + position = (int32_t) long_position; } icu::ParsePosition pp(position); diff --git a/ext/intl/tests/formatter_parse_offset_overflow.phpt b/ext/intl/tests/formatter_parse_offset_overflow.phpt new file mode 100644 index 000000000000..9421c8897511 --- /dev/null +++ b/ext/intl/tests/formatter_parse_offset_overflow.phpt @@ -0,0 +1,46 @@ +--TEST-- +NumberFormatter parse offset overflow +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +getMessage(), PHP_EOL; + } +} + +$offset = PHP_INT_MAX; +print_error(function () use ($fmt, &$offset) { + $fmt->parse('123', NumberFormatter::TYPE_DOUBLE, $offset); +}); + +$offset = PHP_INT_MAX; +print_error(function () use ($fmt, &$offset) { + numfmt_parse($fmt, '123', NumberFormatter::TYPE_DOUBLE, $offset); +}); + +$currency = ''; +$offset = PHP_INT_MAX; +print_error(function () use ($currencyFmt, &$currency, &$offset) { + $currencyFmt->parseCurrency('$123.00', $currency, $offset); +}); + +$currency = ''; +$offset = PHP_INT_MAX; +print_error(function () use ($currencyFmt, &$currency, &$offset) { + numfmt_parse_currency($currencyFmt, '$123.00', $currency, $offset); +}); +?> +--EXPECT-- +ValueError: NumberFormatter::parse(): Argument #3 ($offset) must be between -2147483648 and 2147483647 +ValueError: numfmt_parse(): Argument #4 ($offset) must be between -2147483648 and 2147483647 +ValueError: NumberFormatter::parseCurrency(): Argument #3 ($offset) must be between -2147483648 and 2147483647 +ValueError: numfmt_parse_currency(): Argument #4 ($offset) must be between -2147483648 and 2147483647 From 4629b3e1fa1abd5e720af5c3885756a975db1812 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 3 Jul 2026 13:03:27 -0700 Subject: [PATCH 5/5] [RFC] Allow `#[\Override]` on class constants (#20478) https://wiki.php.net/rfc/override_constants --- UPGRADING | 2 + .../with_Override_error_constant.phpt | 15 +++++ .../with_Override_error_enum_case.phpt | 15 +++++ .../with_Override_okay.phpt | 4 +- .../override/constants/anon_failure.phpt | 15 +++++ .../override/constants/anon_interface.phpt | 19 +++++++ .../override/constants/anon_parent.phpt | 19 +++++++ .../attributes/override/constants/basic.phpt | 57 +++++++++++++++++++ .../override/constants/enum_failure.phpt | 15 +++++ .../override/constants/enum_failure_case.phpt | 15 +++++ .../override/constants/failure.phpt | 15 +++++ .../override/constants/interface_failure.phpt | 15 +++++ .../override/constants/trait_failure.phpt | 19 +++++++ .../override/constants/trait_interface.phpt | 23 ++++++++ .../override/constants/trait_parent.phpt | 23 ++++++++ .../override/constants/trait_redeclared.phpt | 21 +++++++ .../constants/trait_redeclared_interface.phpt | 25 ++++++++ .../constants/trait_redeclared_parent.phpt | 25 ++++++++ .../override/constants/trait_unused.phpt | 15 +++++ .../override/constants/visibility_01.phpt | 19 +++++++ .../override/constants/visibility_02.phpt | 19 +++++++ .../override/constants/visibility_03.phpt | 19 +++++++ .../override/constants/visibility_04.phpt | 19 +++++++ Zend/zend_attributes.stub.php | 2 +- Zend/zend_attributes_arginfo.h | 4 +- Zend/zend_compile.c | 19 +++++++ Zend/zend_inheritance.c | 15 +++++ 27 files changed, 469 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt create mode 100644 Zend/tests/attributes/override/constants/anon_failure.phpt create mode 100644 Zend/tests/attributes/override/constants/anon_interface.phpt create mode 100644 Zend/tests/attributes/override/constants/anon_parent.phpt create mode 100644 Zend/tests/attributes/override/constants/basic.phpt create mode 100644 Zend/tests/attributes/override/constants/enum_failure.phpt create mode 100644 Zend/tests/attributes/override/constants/enum_failure_case.phpt create mode 100644 Zend/tests/attributes/override/constants/failure.phpt create mode 100644 Zend/tests/attributes/override/constants/interface_failure.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_failure.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_interface.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_parent.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_redeclared.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt create mode 100644 Zend/tests/attributes/override/constants/trait_unused.phpt create mode 100644 Zend/tests/attributes/override/constants/visibility_01.phpt create mode 100644 Zend/tests/attributes/override/constants/visibility_02.phpt create mode 100644 Zend/tests/attributes/override/constants/visibility_03.phpt create mode 100644 Zend/tests/attributes/override/constants/visibility_04.phpt diff --git a/UPGRADING b/UPGRADING index a42d5ec355a6..b9c659ccc405 100644 --- a/UPGRADING +++ b/UPGRADING @@ -215,6 +215,8 @@ PHP 8.6 UPGRADE NOTES needing to be present beforehand. . It is now possible to define the __debugInfo() magic method on enums. RFC: https://wiki.php.net/rfc/debugable-enums + . #[\Override] can now be applied to class constants, including enum cases. + RFC: https://wiki.php.net/rfc/override_constants - Curl: . curl_getinfo() return array now includes a new size_delivered key, which diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt new file mode 100644 index 000000000000..9ebb8e4c0202 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_constant.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (class constant) +--FILE-- + +--EXPECTF-- +Fatal error: DemoClass::CLASS_CONSTANT has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt new file mode 100644 index 000000000000..53d75412a849 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_enum_case.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (enum case) +--FILE-- + +--EXPECTF-- +Fatal error: DemoEnum::MyCase has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt index dd077f4b9cbd..494d85eaea9c 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt @@ -12,6 +12,8 @@ class Base { set => $value; } + public const CLASS_CONST = ''; + public function printVal() { echo __METHOD__ . "\n"; } @@ -34,7 +36,7 @@ class DemoClass extends Base { } #[DelayedTargetValidation] - #[Override] // Does nothing here + #[Override] // Does something here public const CLASS_CONST = 'FOO'; public function __construct( diff --git a/Zend/tests/attributes/override/constants/anon_failure.phpt b/Zend/tests/attributes/override/constants/anon_failure.phpt new file mode 100644 index 000000000000..c43cedda81d7 --- /dev/null +++ b/Zend/tests/attributes/override/constants/anon_failure.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - anonymous class, no interface or parent class +--FILE-- + +--EXPECTF-- +Fatal error: class@anonymous::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/anon_interface.phpt b/Zend/tests/attributes/override/constants/anon_interface.phpt new file mode 100644 index 000000000000..b19abfbf4d3a --- /dev/null +++ b/Zend/tests/attributes/override/constants/anon_interface.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - anonymous class overrides interface +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/anon_parent.phpt b/Zend/tests/attributes/override/constants/anon_parent.phpt new file mode 100644 index 000000000000..b430705c3c9d --- /dev/null +++ b/Zend/tests/attributes/override/constants/anon_parent.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - anonymous class overrides parent class +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/basic.phpt b/Zend/tests/attributes/override/constants/basic.phpt new file mode 100644 index 000000000000..c89007022051 --- /dev/null +++ b/Zend/tests/attributes/override/constants/basic.phpt @@ -0,0 +1,57 @@ +--TEST-- +#[\Override]: Constants - basic +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/enum_failure.phpt b/Zend/tests/attributes/override/constants/enum_failure.phpt new file mode 100644 index 000000000000..77ba225e9b9e --- /dev/null +++ b/Zend/tests/attributes/override/constants/enum_failure.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - no interface for enum constant +--FILE-- + +--EXPECTF-- +Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/enum_failure_case.phpt b/Zend/tests/attributes/override/constants/enum_failure_case.phpt new file mode 100644 index 000000000000..2f2c6fc84a08 --- /dev/null +++ b/Zend/tests/attributes/override/constants/enum_failure_case.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - no interface for enum case +--FILE-- + +--EXPECTF-- +Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/failure.phpt b/Zend/tests/attributes/override/constants/failure.phpt new file mode 100644 index 000000000000..be621915d927 --- /dev/null +++ b/Zend/tests/attributes/override/constants/failure.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - no interface or parent class +--FILE-- + +--EXPECTF-- +Fatal error: Demo::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/interface_failure.phpt b/Zend/tests/attributes/override/constants/interface_failure.phpt new file mode 100644 index 000000000000..0aa4d33d1301 --- /dev/null +++ b/Zend/tests/attributes/override/constants/interface_failure.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - no parent interface +--FILE-- + +--EXPECTF-- +Fatal error: IFace::I has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/trait_failure.phpt b/Zend/tests/attributes/override/constants/trait_failure.phpt new file mode 100644 index 000000000000..8d4af0e57977 --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_failure.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - on a trait, no interface or parent class +--FILE-- + +--EXPECTF-- +Fatal error: UsesTrait::T has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/trait_interface.phpt b/Zend/tests/attributes/override/constants/trait_interface.phpt new file mode 100644 index 000000000000..3a2ba799ae4e --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_interface.phpt @@ -0,0 +1,23 @@ +--TEST-- +#[\Override]: Constants - on a trait, overrides interface +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/trait_parent.phpt b/Zend/tests/attributes/override/constants/trait_parent.phpt new file mode 100644 index 000000000000..2753aaf546a2 --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_parent.phpt @@ -0,0 +1,23 @@ +--TEST-- +#[\Override]: Constants - on a trait, overrides parent class +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/trait_redeclared.phpt b/Zend/tests/attributes/override/constants/trait_redeclared.phpt new file mode 100644 index 000000000000..984f9eedb668 --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_redeclared.phpt @@ -0,0 +1,21 @@ +--TEST-- +#[\Override]: Constants - trait constant redeclared, not overridden +--FILE-- + +--EXPECTF-- +Fatal error: UsesTrait::T has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt b/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt new file mode 100644 index 000000000000..21a1aa532249 --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_redeclared_interface.phpt @@ -0,0 +1,25 @@ +--TEST-- +#[\Override]: Constants - trait constant redeclared, overrides interface +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt b/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt new file mode 100644 index 000000000000..c3d1e43d2534 --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_redeclared_parent.phpt @@ -0,0 +1,25 @@ +--TEST-- +#[\Override]: Constants - trait constant redeclared, overrides parent class +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/trait_unused.phpt b/Zend/tests/attributes/override/constants/trait_unused.phpt new file mode 100644 index 000000000000..785f2888e90b --- /dev/null +++ b/Zend/tests/attributes/override/constants/trait_unused.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Override]: Constants - on a trait, unused +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/visibility_01.phpt b/Zend/tests/attributes/override/constants/visibility_01.phpt new file mode 100644 index 000000000000..49671471f758 --- /dev/null +++ b/Zend/tests/attributes/override/constants/visibility_01.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - private constant not overridden (by public constant) +--FILE-- + +--EXPECTF-- +Fatal error: Child::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/visibility_02.phpt b/Zend/tests/attributes/override/constants/visibility_02.phpt new file mode 100644 index 000000000000..c160a29a6180 --- /dev/null +++ b/Zend/tests/attributes/override/constants/visibility_02.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - private constant not overridden (by private constant) +--FILE-- + +--EXPECTF-- +Fatal error: Child::C has #[\Override] attribute, but no matching parent constant exists in %s on line %d diff --git a/Zend/tests/attributes/override/constants/visibility_03.phpt b/Zend/tests/attributes/override/constants/visibility_03.phpt new file mode 100644 index 000000000000..db460f24375e --- /dev/null +++ b/Zend/tests/attributes/override/constants/visibility_03.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - protected constant is overridden (by public constant) +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/tests/attributes/override/constants/visibility_04.phpt b/Zend/tests/attributes/override/constants/visibility_04.phpt new file mode 100644 index 000000000000..0ad46b022f43 --- /dev/null +++ b/Zend/tests/attributes/override/constants/visibility_04.phpt @@ -0,0 +1,19 @@ +--TEST-- +#[\Override]: Constants - protected constant is overridden (by protected constant) +--FILE-- + +--EXPECT-- +Done diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index ded9c89593a3..05fd285d5b9f 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -68,7 +68,7 @@ public function __debugInfo(): array {} /** * @strict-properties */ -#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_PROPERTY)] +#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_PROPERTY|Attribute::TARGET_CLASS_CONSTANT)] final class Override { public function __construct() {} diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index 54a66af29966..8acac398426f 100644 --- a/Zend/zend_attributes_arginfo.h +++ b/Zend/zend_attributes_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_attributes.stub.php instead. - * Stub hash: b868cb33f41d9442f42d0cec84e33fcc09f5d88c */ + * Stub hash: bae42fcb945360f90a1e2762c6c22177f25efb9e */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Attribute___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Attribute::TARGET_ALL") @@ -230,7 +230,7 @@ static zend_class_entry *register_class_Override(void) zend_string *attribute_name_Attribute_class_Override_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, true); zend_attribute *attribute_Attribute_class_Override_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_Override_0, 1); zend_string_release_ex(attribute_name_Attribute_class_Override_0, true); - ZVAL_LONG(&attribute_Attribute_class_Override_0->args[0].value, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_PROPERTY); + ZVAL_LONG(&attribute_Attribute_class_Override_0->args[0].value, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_PROPERTY | ZEND_ATTRIBUTE_TARGET_CLASS_CONST); return class_entry; } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 4c1375215888..c75333e6d3c5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9370,6 +9370,15 @@ static void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_as ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS; ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED; } + + const zend_attribute *override = zend_get_attribute_str(c->attributes, "override", sizeof("override") - 1); + if (override) { + ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_OVERRIDE; + /* We need to be able to remove the flag once the override is + * resolved. See ZEND_ACC_DEPRECATED above. */ + ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS; + ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED; + } } } } @@ -9829,6 +9838,16 @@ static void zend_compile_enum_case(zend_ast *ast) if (deprecated) { ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_DEPRECATED; } + + const zend_attribute *override = zend_get_attribute_str(c->attributes, "override", sizeof("override") - 1); + if (override) { + ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_OVERRIDE; + /* We need to be able to remove the flag once the override is + * resolved. See ZEND_ACC_DEPRECATED handling in + * zend_compile_class_const_decl(). */ + enum_class->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS; + enum_class->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED; + } } } diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 848b9d209b2d..583b10d34188 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2115,6 +2115,12 @@ static bool do_inherit_constant_check( ); } + if (!(ZEND_CLASS_CONST_FLAGS(parent_constant) & ZEND_ACC_PRIVATE)) { + if (child_constant->ce == ce) { + ZEND_CLASS_CONST_FLAGS(child_constant) &= ~ZEND_ACC_OVERRIDE; + } + } + if (!(ZEND_CLASS_CONST_FLAGS(parent_constant) & ZEND_ACC_PRIVATE) && ZEND_TYPE_IS_SET(parent_constant->type)) { inheritance_status status = class_constant_types_compatible(parent_constant, child_constant); if (status == INHERITANCE_ERROR) { @@ -2317,6 +2323,15 @@ void zend_inheritance_check_override(const zend_class_entry *ce) } } ZEND_HASH_FOREACH_END(); + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->constants_table, zend_string *name, zend_class_constant *c) { + if (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_OVERRIDE) { + zend_error_noreturn( + E_COMPILE_ERROR, + "%s::%s has #[\\Override] attribute, but no matching parent constant exists", + ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } + } ZEND_HASH_FOREACH_END(); + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, zend_property_info *prop) { if (prop->flags & ZEND_ACC_OVERRIDE) { zend_error_noreturn(