From cd751f98cb991bae0bdb8de7e9b6970a14e0664a Mon Sep 17 00:00:00 2001 From: DanielEScherzer Date: Sun, 25 May 2025 16:43:36 -0700 Subject: [PATCH] Reapply GH-17712 with a fix for internal class constants (#18464) Add recursion protection when emitting deprecation warnings for class constants, since the deprecation message can come from an attribute that is using the same constant for the message, or otherwise result in recursion. But, internal constants are persisted, and thus cannot have recursion protection. Otherwise, if a user error handler triggers bailout before the recursion flag is removed then a subsequent request (e.g. with `--repeat 2`) would start with that flag already applied. Internal constants can presumably be trusted not to use deprecation messages that come from recursive attributes. Fixes GH-18463 Fixes GH-17711 --- NEWS | 5 ++ .../deprecated/class_constants/gh17711.phpt | 28 ++++++++++ .../constants/gh18463-class-constant.phpt | 20 +++++++ Zend/zend_API.c | 2 +- Zend/zend_compile.c | 4 ++ Zend/zend_constants.c | 9 +++- Zend/zend_constants.h | 11 ++++ Zend/zend_vm_def.h | 9 +++- Zend/zend_vm_execute.h | 54 ++++++++++++++++--- ext/opcache/ZendAccelerator.c | 5 ++ 10 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 Zend/tests/attributes/deprecated/class_constants/gh17711.phpt create mode 100644 Zend/tests/constants/gh18463-class-constant.phpt diff --git a/NEWS b/NEWS index e65b61fe692..45182d73c65 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,11 @@ PHP NEWS . Fixed bug GH-18641 (Accessing a BcMath\Number property by ref crashes). (nielsdos) +- Core: + . Fixed bugs GH-17711 and GH-18022 (Infinite recursion on deprecated attribute + evaluation) and GH-18464 (Recursion protection for deprecation constants not + released on bailout). (DanielEScherzer and ilutov) + - Intl: . Fix memory leak in intl_datetime_decompose() on failure. (nielsdos) diff --git a/Zend/tests/attributes/deprecated/class_constants/gh17711.phpt b/Zend/tests/attributes/deprecated/class_constants/gh17711.phpt new file mode 100644 index 00000000000..abec209343a --- /dev/null +++ b/Zend/tests/attributes/deprecated/class_constants/gh17711.phpt @@ -0,0 +1,28 @@ +--TEST-- +GH-17711: Infinite recursion through deprecated class constants self-referencing through deprecation message +--FILE-- + +--EXPECTF-- +Deprecated: Constant C::C is deprecated, Message in %s on line %d +string(7) "Message" + +Deprecated: Constant D::C is deprecated, test in %s on line %d +string(4) "test" diff --git a/Zend/tests/constants/gh18463-class-constant.phpt b/Zend/tests/constants/gh18463-class-constant.phpt new file mode 100644 index 00000000000..2af977205dc --- /dev/null +++ b/Zend/tests/constants/gh18463-class-constant.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-18463: Recursion protection should not be applied to internal class constants +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECTF-- +Constant _ZendTestClass::ZEND_TEST_DEPRECATED is deprecated in %s on line %d + +Fatal error: Cannot use "string" as a class name as it is reserved in %s(%d) : eval()'d code on line %d diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 5aac3c1f7d7..6e254561d37 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1439,7 +1439,7 @@ ZEND_API HashTable *zend_separate_class_constants_table(zend_class_entry *class_ ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&class_type->constants_table, key, c) { if (c->ce == class_type) { - if (Z_TYPE(c->value) == IS_CONSTANT_AST) { + if (Z_TYPE(c->value) == IS_CONSTANT_AST || (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED)) { new_c = zend_arena_alloc(&CG(arena), sizeof(zend_class_constant)); memcpy(new_c, c, sizeof(zend_class_constant)); c = new_c; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 832dedc4210..195c65d5043 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8822,6 +8822,10 @@ static void zend_compile_class_const_decl(zend_ast *ast, uint32_t flags, zend_as if (deprecated) { ZEND_CLASS_CONST_FLAGS(c) |= ZEND_ACC_DEPRECATED; + /* For deprecated constants, we need to flag the zval for recursion + * detection. Make sure the zval is separated out of shm. */ + ce->ce_flags |= ZEND_ACC_HAS_AST_CONSTANTS; + ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED; } } } diff --git a/Zend/zend_constants.c b/Zend/zend_constants.c index d453b8bb737..50bdb3ddad2 100644 --- a/Zend/zend_constants.c +++ b/Zend/zend_constants.c @@ -353,8 +353,15 @@ ZEND_API zval *zend_get_class_constant_ex(zend_string *class_name, zend_string * } if (UNEXPECTED(ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED)) { - if ((flags & ZEND_FETCH_CLASS_SILENT) == 0) { + if ((flags & ZEND_FETCH_CLASS_SILENT) == 0 && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { goto failure; } diff --git a/Zend/zend_constants.h b/Zend/zend_constants.h index 6f0710c0ce6..bd759c28915 100644 --- a/Zend/zend_constants.h +++ b/Zend/zend_constants.h @@ -27,6 +27,17 @@ #define CONST_NO_FILE_CACHE (1<<1) /* Can't be saved in file cache */ #define CONST_DEPRECATED (1<<2) /* Deprecated */ #define CONST_OWNED (1<<3) /* constant should be destroyed together with class */ +#define CONST_RECURSIVE (1<<4) /* Recursion protection for constant evaluation */ + +#define CONST_IS_RECURSIVE(c) (Z_CONSTANT_FLAGS((c)->value) & CONST_RECURSIVE) +#define CONST_PROTECT_RECURSION(c) \ + do { \ + Z_CONSTANT_FLAGS((c)->value) |= CONST_RECURSIVE; \ + } while (0) +#define CONST_UNPROTECT_RECURSION(c) \ + do { \ + Z_CONSTANT_FLAGS((c)->value) &= ~CONST_RECURSIVE; \ + } while (0) #define PHP_USER_CONSTANT 0x7fffff /* a constant defined in user space */ diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 2eecfe035ea..01bdcf0ad6a 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6094,8 +6094,15 @@ ZEND_VM_HANDLER(181, ZEND_FETCH_CLASS_CONSTANT, VAR|CONST|UNUSED|CLASS_FETCH, CO } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 40cbb495870..25e537dc1c0 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -7615,8 +7615,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_CONS } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -8776,8 +8783,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_CONS } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -25882,8 +25896,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_ } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -26452,8 +26473,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_VAR_ } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -35294,8 +35322,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUS } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); @@ -35654,8 +35689,15 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_FETCH_CLASS_CONSTANT_SPEC_UNUS } bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED; - if (UNEXPECTED(is_constant_deprecated)) { + if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) { + if (c->ce->type == ZEND_USER_CLASS) { + /* Recursion protection only applied to user constants, GH-18463 */ + CONST_PROTECT_RECURSION(c); + } zend_deprecated_class_constant(c, constant_name); + if (c->ce->type == ZEND_USER_CLASS) { + CONST_UNPROTECT_RECURSION(c); + } if (EG(exception)) { ZVAL_UNDEF(EX_VAR(opline->result.var)); diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 82ae1adce44..08abd7048d6 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -3806,6 +3806,11 @@ static bool preload_try_resolve_constants(zend_class_entry *ce) ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->constants_table, key, c) { val = &c->value; if (Z_TYPE_P(val) == IS_CONSTANT_AST) { + /* For deprecated constants, we need to flag the zval for recursion + * detection. Make sure the zval is separated out of shm. */ + if (ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED) { + ok = false; + } if (EXPECTED(zend_update_class_constant(c, key, c->ce) == SUCCESS)) { was_changed = changed = true; } else {