From 75c4434fe1e7a0e07c53c938d4031497c3990725 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:59:03 +0100 Subject: [PATCH] Fix GH-12895: Incorrect ReflectionClass::getNamespaceName for anonymous class Because paths can contain backslashes, searching for the first backslash starting on the right for detecting namespaces yields the wrong result. Furthermore, because the namespace of the class is not actually in the name (only the interface or parent class is) the namespace can be wrong. This fix consists of two parts: - Fix how a prefix is chosen such that the correct namespace is included in the name. - If ZEND_ACC_ANON_CLASS is set, check the position of @ and search for the backslash before that. While the issue seems Windows-specific, I could even reproduce this on Linux because Linux allows directories to contain backslashes. --- Zend/tests/anon/006.phpt | 4 +- Zend/tests/anon/007.phpt | 4 +- Zend/tests/anon_class_name.phpt | 10 ++-- ...ation_to_exception_during_inheritance.phpt | 2 +- Zend/tests/generators/gh8289.phpt | 2 +- .../object_types/return_type_in_class.phpt | 4 +- .../return_type_inheritance_in_class.phpt | 4 +- .../return_type_inheritance_in_interface.phpt | 4 +- Zend/tests/record_errors_001.phpt | 2 +- Zend/tests/temporary_cleaning_013.phpt | 4 +- .../typed_properties_065.phpt | 8 +-- Zend/zend_compile.c | 17 +++---- ext/date/tests/bug65672.phpt | 6 +-- ext/opcache/tests/bug78937_1.phpt | 4 +- ext/opcache/tests/bug78937_2.phpt | 4 +- ext/opcache/tests/bug78937_3.phpt | 2 +- ext/opcache/tests/bug78937_4.phpt | 2 +- ext/opcache/tests/bug78937_5.phpt | 2 +- ext/opcache/tests/bug78937_6.phpt | 2 +- ext/reflection/php_reflection.c | 24 +++++++-- .../ReflectionClass_isSubclassOf_error2.phpt | 2 +- ext/reflection/tests/gh12895.phpt | 50 +++++++++++++++++++ ext/standard/tests/class_object/bug78638.phpt | 2 +- .../get_object_vars_variation_004.phpt | 4 +- .../get_debug_type_basic.phpt | 4 +- 25 files changed, 120 insertions(+), 53 deletions(-) create mode 100644 ext/reflection/tests/gh12895.phpt diff --git a/Zend/tests/anon/006.phpt b/Zend/tests/anon/006.phpt index 6bcd252dfc383..614a7a997e4e2 100644 --- a/Zend/tests/anon/006.phpt +++ b/Zend/tests/anon/006.phpt @@ -10,6 +10,6 @@ namespace { var_dump ($hello); } ?> ---EXPECTF-- -object(class@%s)#1 (0) { +--EXPECT-- +object(lone\@anonymous)#1 (0) { } diff --git a/Zend/tests/anon/007.phpt b/Zend/tests/anon/007.phpt index 166444b64f46c..0f01d759464e5 100644 --- a/Zend/tests/anon/007.phpt +++ b/Zend/tests/anon/007.phpt @@ -18,6 +18,6 @@ namespace lone { new Outer(); } ?> ---EXPECTF-- -object(class@%s)#2 (0) { +--EXPECT-- +object(lone\@anonymous)#2 (0) { } diff --git a/Zend/tests/anon_class_name.phpt b/Zend/tests/anon_class_name.phpt index 36f613d707b1c..ed0f40b3b5af1 100644 --- a/Zend/tests/anon_class_name.phpt +++ b/Zend/tests/anon_class_name.phpt @@ -23,8 +23,8 @@ namespace UsingNS { ?> --EXPECT-- -class@anonymous -DeclaringNS\Test1@anonymous -DeclaringNS\Test1@anonymous -DeclaringNS\Test2@anonymous -DeclaringNS\Test2@anonymous +UsingNS\@anonymous +UsingNS\@anonymous +UsingNS\@anonymous +UsingNS\@anonymous +UsingNS\@anonymous diff --git a/Zend/tests/deprecation_to_exception_during_inheritance.phpt b/Zend/tests/deprecation_to_exception_during_inheritance.phpt index dd0adec36a57c..1569f1770026d 100644 --- a/Zend/tests/deprecation_to_exception_during_inheritance.phpt +++ b/Zend/tests/deprecation_to_exception_during_inheritance.phpt @@ -17,7 +17,7 @@ $class = new class extends DateTime { ?> --EXPECTF-- -Fatal error: During inheritance of DateTime: Uncaught Exception: Return type of DateTime@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s:%d +Fatal error: During inheritance of DateTime: Uncaught Exception: Return type of class@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s:%d Stack trace: #0 %s(%d): {closure}(8192, 'Return type of ...', '%s', 8) #1 {main} in %s on line %d diff --git a/Zend/tests/generators/gh8289.phpt b/Zend/tests/generators/gh8289.phpt index f5bb1fc64be0b..4ffa6038305e1 100644 --- a/Zend/tests/generators/gh8289.phpt +++ b/Zend/tests/generators/gh8289.phpt @@ -28,7 +28,7 @@ foreach (yieldFromIteratorGeneratorThrows() as $k => $v) { int(1) Exception in %s:%d Stack trace: -#0 %s(%d): IteratorIterator@anonymous->key() +#0 %s(%d): class@anonymous->key() #1 %s(%d): yieldFromIteratorGeneratorThrows() #2 {main} int(2) diff --git a/Zend/tests/object_types/return_type_in_class.phpt b/Zend/tests/object_types/return_type_in_class.phpt index 3e441073737e3..0d21276afb1d3 100644 --- a/Zend/tests/object_types/return_type_in_class.phpt +++ b/Zend/tests/object_types/return_type_in_class.phpt @@ -19,8 +19,8 @@ $three = new class extends Two { $three->a(); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: Two@anonymous::a(): Return value must be of type object, int returned in %s:%d +Fatal error: Uncaught TypeError: class@anonymous::a(): Return value must be of type object, int returned in %s:%d Stack trace: -#0 %s(%d): Two@anonymous->a() +#0 %s(%d): class@anonymous->a() #1 {main} thrown in %s on line 13 diff --git a/Zend/tests/object_types/return_type_inheritance_in_class.phpt b/Zend/tests/object_types/return_type_inheritance_in_class.phpt index 6e2dbac55cf21..1047f109095a3 100644 --- a/Zend/tests/object_types/return_type_inheritance_in_class.phpt +++ b/Zend/tests/object_types/return_type_inheritance_in_class.phpt @@ -19,8 +19,8 @@ $three = new class extends Two { $three->a(); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: Two@anonymous::a(): Return value must be of type object, int returned in %s:%d +Fatal error: Uncaught TypeError: class@anonymous::a(): Return value must be of type object, int returned in %s:%d Stack trace: -#0 %s(%d): Two@anonymous->a() +#0 %s(%d): class@anonymous->a() #1 {main} thrown in %s on line 13 diff --git a/Zend/tests/object_types/return_type_inheritance_in_interface.phpt b/Zend/tests/object_types/return_type_inheritance_in_interface.phpt index 8bb5ad1616e9a..ccff2eaf8b3fe 100644 --- a/Zend/tests/object_types/return_type_inheritance_in_interface.phpt +++ b/Zend/tests/object_types/return_type_inheritance_in_interface.phpt @@ -19,8 +19,8 @@ $three = new class implements Two { $three->a(); ?> --EXPECTF-- -Fatal error: Uncaught TypeError: Two@anonymous::a(): Return value must be of type object, int returned in %s:%d +Fatal error: Uncaught TypeError: class@anonymous::a(): Return value must be of type object, int returned in %s:%d Stack trace: -#0 %s(%d): Two@anonymous->a() +#0 %s(%d): class@anonymous->a() #1 {main} thrown in %s on line 13 diff --git a/Zend/tests/record_errors_001.phpt b/Zend/tests/record_errors_001.phpt index dd3c0ba487ea0..962ad2b2b8bc4 100644 --- a/Zend/tests/record_errors_001.phpt +++ b/Zend/tests/record_errors_001.phpt @@ -16,4 +16,4 @@ new class extends DateTime { }; ?> --EXPECT-- -Error: Return type of DateTime@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice +Error: Return type of class@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice diff --git a/Zend/tests/temporary_cleaning_013.phpt b/Zend/tests/temporary_cleaning_013.phpt index e5f4b73bb14a2..be901744f5afb 100644 --- a/Zend/tests/temporary_cleaning_013.phpt +++ b/Zend/tests/temporary_cleaning_013.phpt @@ -301,10 +301,10 @@ caught Exception 13 Deprecated: Creation of dynamic property class@anonymous::$foo is deprecated in %s on line %d caught Exception 14 -Notice: Indirect modification of overloaded element of ArrayAccess@anonymous has no effect in %s on line %d +Notice: Indirect modification of overloaded element of class@anonymous has no effect in %s on line %d caught Exception 15 -Notice: Indirect modification of overloaded element of ArrayAccess@anonymous has no effect in %s on line %d +Notice: Indirect modification of overloaded element of class@anonymous has no effect in %s on line %d caught Exception 16 caught Exception 17 caught Exception 18 diff --git a/Zend/tests/type_declarations/typed_properties_065.phpt b/Zend/tests/type_declarations/typed_properties_065.phpt index a3a536da07e1c..d3a8a80cf726a 100644 --- a/Zend/tests/type_declarations/typed_properties_065.phpt +++ b/Zend/tests/type_declarations/typed_properties_065.phpt @@ -61,11 +61,11 @@ offsetSet(1e50) int(1) int(0) int(-1) -Cannot decrement a reference held by property ArrayAccess@anonymous::$foo of type int past its minimal value +Cannot decrement a reference held by property class@anonymous::$foo of type int past its minimal value integer -Cannot decrement a reference held by property ArrayAccess@anonymous::$foo of type int past its minimal value +Cannot decrement a reference held by property class@anonymous::$foo of type int past its minimal value integer -Cannot increment a reference held by property ArrayAccess@anonymous::$foo of type int past its maximal value +Cannot increment a reference held by property class@anonymous::$foo of type int past its maximal value integer -Cannot increment a reference held by property ArrayAccess@anonymous::$foo of type int past its maximal value +Cannot increment a reference held by property class@anonymous::$foo of type int past its maximal value integer diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index cba90112cfc18..450dc721191b0 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7833,18 +7833,17 @@ static zend_string *zend_generate_anon_class_name(zend_ast_decl *decl) zend_string *filename = CG(active_op_array)->filename; uint32_t start_lineno = decl->start_lineno; - /* Use parent or first interface as prefix. */ + /* Use current_namespace\\ or "class" as prefix */ zend_string *prefix = ZSTR_KNOWN(ZEND_STR_CLASS); - if (decl->child[0]) { - prefix = zend_resolve_const_class_name_reference(decl->child[0], "class name"); - } else if (decl->child[1]) { - zend_ast_list *list = zend_ast_get_list(decl->child[1]); - prefix = zend_resolve_const_class_name_reference(list->child[0], "interface name"); + const char *prefix_suffix = ""; + if (FC(current_namespace)) { + prefix = FC(current_namespace); + /* This is necessary to get the right short name of anonymous classes. */ + prefix_suffix = "\\"; } - zend_string *result = zend_strpprintf(0, "%s@anonymous%c%s:%" PRIu32 "$%" PRIx32, - ZSTR_VAL(prefix), '\0', ZSTR_VAL(filename), start_lineno, CG(rtd_key_counter)++); - zend_string_release(prefix); + zend_string *result = zend_strpprintf(0, "%s%s@anonymous%c%s:%" PRIu32 "$%" PRIx32, + ZSTR_VAL(prefix), prefix_suffix, '\0', ZSTR_VAL(filename), start_lineno, CG(rtd_key_counter)++); return zend_new_interned_string(result); } diff --git a/ext/date/tests/bug65672.phpt b/ext/date/tests/bug65672.phpt index a3a4542e69db2..240e12d54c872 100644 --- a/ext/date/tests/bug65672.phpt +++ b/ext/date/tests/bug65672.phpt @@ -41,16 +41,16 @@ array(1) { } bool(false) -Deprecated: Creation of dynamic property DatePeriod@anonymous::$dynamic1 is deprecated in %s on line %d +Deprecated: Creation of dynamic property class@anonymous::$dynamic1 is deprecated in %s on line %d string(7) "dynamic" -Deprecated: Creation of dynamic property DatePeriod@anonymous::$dynamic2 is deprecated in %s on line %d +Deprecated: Creation of dynamic property class@anonymous::$dynamic2 is deprecated in %s on line %d array(1) { [0]=> string(5) "array" } -Deprecated: Creation of dynamic property DatePeriod@anonymous::$dynamic3 is deprecated in %s on line %d +Deprecated: Creation of dynamic property class@anonymous::$dynamic3 is deprecated in %s on line %d array(1) { [0]=> string(5) "array" diff --git a/ext/opcache/tests/bug78937_1.phpt b/ext/opcache/tests/bug78937_1.phpt index 657b306d68d4d..79f7423b2ef34 100644 --- a/ext/opcache/tests/bug78937_1.phpt +++ b/ext/opcache/tests/bug78937_1.phpt @@ -18,6 +18,6 @@ class Bar { var_dump(foo()); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 -object(Bar@anonymous)#%d (0) { +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +object(class@anonymous)#1 (0) { } diff --git a/ext/opcache/tests/bug78937_2.phpt b/ext/opcache/tests/bug78937_2.phpt index eb359cee96bea..2cfccf31d88a0 100644 --- a/ext/opcache/tests/bug78937_2.phpt +++ b/ext/opcache/tests/bug78937_2.phpt @@ -19,6 +19,6 @@ class Bar { var_dump(foo()); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 -object(Bar@anonymous)#%d (0) { +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +object(class@anonymous)#1 (0) { } diff --git a/ext/opcache/tests/bug78937_3.phpt b/ext/opcache/tests/bug78937_3.phpt index b17f8d0a08c3b..a3393aeaf2fcf 100644 --- a/ext/opcache/tests/bug78937_3.phpt +++ b/ext/opcache/tests/bug78937_3.phpt @@ -17,7 +17,7 @@ include(__DIR__ . "/preload_bug78937.inc"); var_dump(foo()); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 Fatal error: Uncaught Error: Class "Bar" not found in %spreload_bug78937.inc:3 Stack trace: diff --git a/ext/opcache/tests/bug78937_4.phpt b/ext/opcache/tests/bug78937_4.phpt index 2598b5db9c166..6be21cd2d5d1a 100644 --- a/ext/opcache/tests/bug78937_4.phpt +++ b/ext/opcache/tests/bug78937_4.phpt @@ -19,6 +19,6 @@ bar(); var_dump(new Foo); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 object(Foo)#%d (0) { } diff --git a/ext/opcache/tests/bug78937_5.phpt b/ext/opcache/tests/bug78937_5.phpt index dc67b9045696b..e0395e2c0bf4d 100644 --- a/ext/opcache/tests/bug78937_5.phpt +++ b/ext/opcache/tests/bug78937_5.phpt @@ -20,6 +20,6 @@ bar(); var_dump(new Foo); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 object(Foo)#%d (0) { } diff --git a/ext/opcache/tests/bug78937_6.phpt b/ext/opcache/tests/bug78937_6.phpt index fe9d5176aacb2..b86a57323906f 100644 --- a/ext/opcache/tests/bug78937_6.phpt +++ b/ext/opcache/tests/bug78937_6.phpt @@ -18,7 +18,7 @@ bar(); var_dump(new Foo); ?> --EXPECTF-- -Warning: Can't preload unlinked class Bar@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 +Warning: Can't preload unlinked class class@anonymous: Unknown parent Bar in %spreload_bug78937.inc on line 3 Fatal error: Uncaught Error: Class "Bar" not found in %spreload_bug78937.inc:6 Stack trace: diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 136d69b2864f4..3bb4bdb2fb138 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5397,6 +5397,24 @@ ZEND_METHOD(ReflectionClass, getExtensionName) } /* }}} */ +static const char *_class_get_namespace_backslash(const zend_class_entry *ce) +{ + const zend_string *name = ce->name; + size_t name_length; + + if (ce->ce_flags & ZEND_ACC_ANON_CLASS) { + /* Paths may contain backslashes. This is true on Linux and especially on Windows. + * Search first for '@' and then look for the first '\' before that. */ + const char *at = zend_memrchr(ZSTR_VAL(name), '@', ZSTR_LEN(name)); + ZEND_ASSERT(at != NULL); + name_length = at - ZSTR_VAL(name); + } else { + name_length = ZSTR_LEN(name); + } + + return zend_memrchr(ZSTR_VAL(name), '\\', name_length); +} + /* {{{ Returns whether this class is defined in namespace */ ZEND_METHOD(ReflectionClass, inNamespace) { @@ -5410,7 +5428,7 @@ ZEND_METHOD(ReflectionClass, inNamespace) GET_REFLECTION_OBJECT_PTR(ce); zend_string *name = ce->name; - const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + const char *backslash = _class_get_namespace_backslash(ce); RETURN_BOOL(backslash && backslash > ZSTR_VAL(name)); } /* }}} */ @@ -5428,7 +5446,7 @@ ZEND_METHOD(ReflectionClass, getNamespaceName) GET_REFLECTION_OBJECT_PTR(ce); zend_string *name = ce->name; - const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + const char *backslash = _class_get_namespace_backslash(ce); if (backslash && backslash > ZSTR_VAL(name)) { RETURN_STRINGL(ZSTR_VAL(name), backslash - ZSTR_VAL(name)); } @@ -5449,7 +5467,7 @@ ZEND_METHOD(ReflectionClass, getShortName) GET_REFLECTION_OBJECT_PTR(ce); zend_string *name = ce->name; - const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + const char *backslash = _class_get_namespace_backslash(ce); if (backslash && backslash > ZSTR_VAL(name)) { RETURN_STRINGL(backslash + 1, ZSTR_LEN(name) - (backslash - ZSTR_VAL(name) + 1)); } diff --git a/ext/reflection/tests/ReflectionClass_isSubclassOf_error2.phpt b/ext/reflection/tests/ReflectionClass_isSubclassOf_error2.phpt index f3701321fa7b4..5dcaf419812c9 100644 --- a/ext/reflection/tests/ReflectionClass_isSubclassOf_error2.phpt +++ b/ext/reflection/tests/ReflectionClass_isSubclassOf_error2.phpt @@ -34,6 +34,6 @@ echo "Done\n"; ?> --EXPECTF-- After first check -Checking for Base@%s +Checking for class@%s true Done diff --git a/ext/reflection/tests/gh12895.phpt b/ext/reflection/tests/gh12895.phpt new file mode 100644 index 0000000000000..eb8659c3577c6 --- /dev/null +++ b/ext/reflection/tests/gh12895.phpt @@ -0,0 +1,50 @@ +--TEST-- +GH-12895 (Incorrect ReflectionClass::getNamespaceName for anonymous class) +--FILE-- +getNamespaceName(), PHP_EOL; + echo 'getShortName: ', $rc->getShortName(), PHP_EOL; + echo 'inNamespace: ', $rc->inNamespace() ? "true" : "false", PHP_EOL; + echo "---\n"; + } +} + +?> +--EXPECTF-- +Class: Bar\@anonymous%s +getNamespaceName: Bar +getShortName: @anonymous%s +inNamespace: true +--- +Class: Bar\Baz\@anonymous%s +getNamespaceName: Bar\Baz +getShortName: @anonymous%s +inNamespace: true +--- +Class: class@anonymous%s +getNamespaceName: +getShortName: class@anonymous%s +inNamespace: false +--- diff --git a/ext/standard/tests/class_object/bug78638.phpt b/ext/standard/tests/class_object/bug78638.phpt index aaadcda836e71..3eadbde26e610 100644 --- a/ext/standard/tests/class_object/bug78638.phpt +++ b/ext/standard/tests/class_object/bug78638.phpt @@ -6,4 +6,4 @@ $c = new class('bar') extends __PHP_Incomplete_Class { }; ?> --EXPECTF-- -Fatal error: Class __PHP_Incomplete_Class@anonymous cannot extend final class __PHP_Incomplete_Class in %s on line %d +Fatal error: Class class@anonymous cannot extend final class __PHP_Incomplete_Class in %s on line %d diff --git a/ext/standard/tests/class_object/get_object_vars_variation_004.phpt b/ext/standard/tests/class_object/get_object_vars_variation_004.phpt index 84827134c1b18..031b345e60ece 100644 --- a/ext/standard/tests/class_object/get_object_vars_variation_004.phpt +++ b/ext/standard/tests/class_object/get_object_vars_variation_004.phpt @@ -32,7 +32,7 @@ array(4) { [12]=> int(6) ["test"]=> - object(JsonSerializable@anonymous)#2 (0) { + object(class@anonymous)#2 (0) { } } array(4) { @@ -43,6 +43,6 @@ array(4) { [12]=> int(6) ["test"]=> - object(JsonSerializable@anonymous)#2 (0) { + object(class@anonymous)#2 (0) { } } diff --git a/ext/standard/tests/general_functions/get_debug_type_basic.phpt b/ext/standard/tests/general_functions/get_debug_type_basic.phpt index f630265e8cdbb..580c7466e8d53 100644 --- a/ext/standard/tests/general_functions/get_debug_type_basic.phpt +++ b/ext/standard/tests/general_functions/get_debug_type_basic.phpt @@ -45,8 +45,8 @@ namespace { ClassInGlobal Demo\ClassInNamespace class@anonymous -ToBeExtended@anonymous -ToBeImplemented@anonymous +class@anonymous +class@anonymous string bool bool