From 6260e3332239c812b6ee7f451516cd87751791f5 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 30 Apr 2025 12:30:23 -0700 Subject: [PATCH] Reapply GH-17712 with a fix for internal class constants 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 e65b61fe6923d..45182d73c65e5 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 0000000000000..abec209343ab3 --- /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 0000000000000..2af977205dc8d --- /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 5aac3c1f7d77c..6e254561d3745 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 832dedc421042..195c65d504374 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 d453b8bb73717..50bdb3ddad290 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 6f0710c0ce63e..bd759c2891500 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 2eecfe035ea2f..01bdcf0ad6a00 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 40cbb4958708f..25e537dc1c07a 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 82ae1adce44c5..08abd7048d6dd 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 {