From 63e26ab71620a4f636bde1d6c657f9aca5ce987e Mon Sep 17 00:00:00 2001 From: Khaled Alam Date: Tue, 21 Jul 2026 01:32:23 +0400 Subject: [PATCH 1/3] Add trait support for internal classes (GH-20976) Fixes GH-20974 Co-authored-by: Ilija Tovilo --- UPGRADING.INTERNALS | 1 + Zend/zend_API.h | 1 + Zend/zend_inheritance.c | 55 +++++++++++- Zend/zend_opcode.c | 9 +- build/gen_stub.php | 29 +++++++ ext/zend_test/test.c | 18 ++++ ext/zend_test/test.stub.php | 26 ++++++ ext/zend_test/test_arginfo.h | 87 ++++++++++++++++++- ext/zend_test/test_decl.h | 8 +- ext/zend_test/test_legacy_arginfo.h | 87 ++++++++++++++++++- .../tests/internal_class_traits.phpt | 47 ++++++++++ 11 files changed, 359 insertions(+), 9 deletions(-) create mode 100644 ext/zend_test/tests/internal_class_traits.phpt diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index ae58e7ba8d5b..ef5155e39db6 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -146,6 +146,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed. . Added zend_string_equals_cstr_ci(). . Added zend_string_ends_with() and related variants. + . Added trait support for internal classes. ======================== 2. Build system changes diff --git a/Zend/zend_API.h b/Zend/zend_API.h index bbfe64257744..2c0b892d1941 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -395,6 +395,7 @@ ZEND_API zend_class_entry *zend_register_internal_class_ex(const zend_class_entr ZEND_API zend_class_entry *zend_register_internal_class_with_flags(const zend_class_entry *class_entry, zend_class_entry *parent_ce, uint32_t flags); ZEND_API zend_class_entry *zend_register_internal_interface(const zend_class_entry *orig_class_entry); ZEND_API void zend_class_implements(zend_class_entry *class_entry, int num_interfaces, ...); +ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...); ZEND_API zend_result zend_register_class_alias_ex(const char *name, size_t name_len, zend_class_entry *ce, bool persistent); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index 583b10d34188..4424c9a1a3ab 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -2403,7 +2403,11 @@ static void zend_add_trait_method(zend_class_entry *ce, zend_string *name, zend_ } } - if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) { + if (ce->type == ZEND_INTERNAL_CLASS) { + ZEND_ASSERT(fn->type == ZEND_INTERNAL_FUNCTION); + new_fn = (zend_function*)(uintptr_t)malloc(sizeof(zend_internal_function)); + memcpy(new_fn, fn, sizeof(zend_internal_function)); + } else if (UNEXPECTED(fn->type == ZEND_INTERNAL_FUNCTION)) { new_fn = zend_arena_alloc(&CG(arena), sizeof(zend_internal_function)); memcpy(new_fn, fn, sizeof(zend_internal_function)); new_fn->common.fn_flags |= ZEND_ACC_ARENA_ALLOCATED; @@ -2833,7 +2837,11 @@ static void zend_do_traits_constant_binding(zend_class_entry *ce, zend_class_ent if (do_trait_constant_check(ce, constant, constant_name, traits, i)) { zend_class_constant *ct = NULL; - ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant)); + if (ce->type == ZEND_INTERNAL_CLASS) { + ct = malloc(sizeof(zend_class_constant)); + } else { + ct = zend_arena_alloc(&CG(arena),sizeof(zend_class_constant)); + } memcpy(ct, constant, sizeof(zend_class_constant)); constant = ct; @@ -3012,6 +3020,49 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent } /* }}} */ +ZEND_API void zend_class_use_internal_traits(zend_class_entry *class_entry, int num_traits, ...) +{ + ZEND_ASSERT(class_entry->ce_flags & ZEND_ACC_LINKED); + ZEND_ASSERT(num_traits >= 0); + + if (UNEXPECTED(num_traits == 0)) { + return; + } + + zend_class_entry **traits = safe_pemalloc(num_traits, sizeof(zend_class_entry *), 0, /* persistent */ true); + class_entry->trait_names = safe_pemalloc(num_traits, sizeof(zend_class_name), 0, /* persistent */ true); + class_entry->num_traits = num_traits; + + va_list trait_list; + va_start(trait_list, num_traits); + for (int i = 0; i < num_traits; i++) { + zend_class_entry *trait_entry = va_arg(trait_list, zend_class_entry *); + class_entry->trait_names[i].name = zend_string_copy(trait_entry->name); + class_entry->trait_names[i].lc_name = zend_string_tolower_ex(zend_string_copy(trait_entry->name), /* persistent */ true); + + if (UNEXPECTED(!(trait_entry->ce_flags & ZEND_ACC_TRAIT))) { + free(traits); + zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot use %s - it is not a trait", + ZSTR_VAL(class_entry->name), ZSTR_VAL(trait_entry->name)); + } + traits[i] = trait_entry; + } + va_end(trait_list); + + bool contains_abstract_methods = false; + zend_do_traits_method_binding(class_entry, traits, NULL, NULL, false, &contains_abstract_methods); + zend_do_traits_constant_binding(class_entry, traits); + zend_do_traits_property_binding(class_entry, traits); + + ZEND_HASH_MAP_FOREACH_PTR(&class_entry->function_table, zend_function *fn) { + zend_fixup_trait_method(fn, class_entry); + } ZEND_HASH_FOREACH_END(); + + free(traits); + + /* TODO: Verify abstract trait method implementation requirements are enforced. */ +} + #define MAX_ABSTRACT_INFO_CNT 3 #define MAX_ABSTRACT_INFO_FMT "%s%s%s%s" #define DISPLAY_ABSTRACT_FN(idx) \ diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 538eff3ea34d..a25f236c3c20 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -485,7 +485,7 @@ ZEND_API void destroy_zend_class(zval *zv) zend_string_release_ex(ce->name, 1); ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) { - if (fn->common.scope == ce) { + if (fn->common.scope == ce && !(fn->common.fn_flags & ZEND_ACC_TRAIT_CLONE)) { zend_free_internal_arg_info(&fn->internal_function, true); if (fn->common.attributes) { @@ -535,6 +535,13 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->attributes) { zend_hash_release(ce->attributes); } + if (ce->num_traits > 0) { + for (uint32_t i = 0; i < ce->num_traits; i++) { + zend_string_release(ce->trait_names[i].name); + zend_string_release(ce->trait_names[i].lc_name); + } + free(ce->trait_names); + } free(ce); break; } diff --git a/build/gen_stub.php b/build/gen_stub.php index 715a47182340..b2f38c978cf3 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -13,6 +13,7 @@ use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; +use PhpParser\Node\Stmt\TraitUse; use PhpParser\PrettyPrinter\Standard; use PhpParser\PrettyPrinterAbstract; @@ -3383,6 +3384,7 @@ class ClassInfo { * @param AttributeInfo[] $attributes * @param Name[] $extends * @param Name[] $implements + * @param Name[] $uses * @param ConstInfo[] $constInfos * @param PropertyInfo[] $propertyInfos * @param FuncInfo[] $funcInfos @@ -3401,6 +3403,7 @@ public function __construct( private bool $isNotSerializable, private readonly array $extends, private readonly array $implements, + private readonly array $uses, public /* readonly */ array $constInfos, private /* readonly */ array $propertyInfos, public array $funcInfos, @@ -3421,6 +3424,9 @@ public function getRegistration(array $allConstInfos): string foreach ($this->implements as $implements) { $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->getParts()); } + foreach ($this->uses as $use) { + $params[] = "zend_class_entry *class_entry_" . implode("_", $use->getParts()); + } $escapedName = implode("_", $this->name->getParts()); @@ -3518,6 +3524,17 @@ function (Name $item) { $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n"; } + $traits = array_map( + function (Name $item) { + return "class_entry_" . implode("_", $item->getParts()); + }, + $this->uses + ); + + if (!empty($traits)) { + $code .= "\tzend_class_use_internal_traits(class_entry, " . count($traits) . ", " . implode(", ", $traits) . ");\n"; + } + if ($this->alias) { $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n"; } @@ -4408,6 +4425,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri $propertyInfos = []; $methodInfos = []; $enumCaseInfos = []; + $traitUses = []; foreach ($stmt->stmts as $classStmt) { $cond = self::handlePreprocessorConditions($conds, $classStmt); if ($classStmt instanceof Stmt\Nop) { @@ -4469,6 +4487,13 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri $classStmt->expr, $classStmt->expr ? $prettyPrinter->prettyPrintExpr($classStmt->expr) : null, ); + } else if ($classStmt instanceof TraitUse) { + if ($classStmt->adaptations) { + throw new Exception("Trait adaptations are not supported"); + } + foreach ($classStmt->traits as $trait) { + $traitUses[] = $trait; + } } else { throw new Exception("Not implemented {$classStmt->getType()}"); } @@ -4481,6 +4506,7 @@ private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPri $propertyInfos, $methodInfos, $enumCaseInfos, + $traitUses, $cond, $this->getMinimumPhpVersionIdCompatibility(), $this->isUndocumentable @@ -5167,6 +5193,7 @@ function parseProperty( * @param PropertyInfo[] $properties * @param FuncInfo[] $methods * @param EnumCaseInfo[] $enumCases + * @param Name[] $traitUses */ function parseClass( Name $name, @@ -5175,6 +5202,7 @@ function parseClass( array $properties, array $methods, array $enumCases, + array $traitUses, ?string $cond, ?int $minimumPhpVersionIdCompatibility, bool $isUndocumentable @@ -5247,6 +5275,7 @@ function parseClass( $isNotSerializable, $extends, $implements, + $traitUses, $consts, $properties, $methods, diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index e8058936b6da..a880c09fc1ff 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -54,6 +54,9 @@ static zend_class_entry *zend_test_child_class; static zend_class_entry *zend_test_gen_stub_flag_compatibility_test; static zend_class_entry *zend_attribute_test_class; static zend_class_entry *zend_test_trait; +static zend_class_entry *zend_test_trait_for_internal_class; +static zend_class_entry *zend_test_trait_for_internal_class2; +static zend_class_entry *zend_test_class_with_traits; static zend_class_entry *zend_test_attribute; static zend_class_entry *zend_test_repeatable_attribute; static zend_class_entry *zend_test_parameter_attribute; @@ -1305,6 +1308,18 @@ static ZEND_METHOD(_ZendTestTrait, testMethod) RETURN_TRUE; } +static ZEND_METHOD(_ZendTestTraitForInternalClass, traitMethod) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_LONG(789); +} + +static ZEND_METHOD(_ZendTestTraitForInternalClass2, traitMethod2) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_LONG(101); +} + static ZEND_METHOD(ZendTestNS_Foo, method) { ZEND_PARSE_PARAMETERS_NONE(); @@ -1607,6 +1622,9 @@ PHP_MINIT_FUNCTION(zend_test) zend_attribute_test_class = register_class_ZendAttributeTest(); zend_test_trait = register_class__ZendTestTrait(); + zend_test_trait_for_internal_class = register_class__ZendTestTraitForInternalClass(); + zend_test_trait_for_internal_class2 = register_class__ZendTestTraitForInternalClass2(); + zend_test_class_with_traits = register_class__ZendTestClassWithTraits(zend_test_trait_for_internal_class, zend_test_trait_for_internal_class2); register_test_symbols(module_number); diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index a4562368735e..7c96ea176a88 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -35,6 +35,32 @@ interface _ZendTestInterface public const DUMMY = 0; } + trait _ZendTestTraitForInternalClass + { + /** @var int */ + public const ZEND_TRAIT_CONST = 123; + + public int $traitProp = 456; + + public function traitMethod(): int {} + } + + trait _ZendTestTraitForInternalClass2 + { + /** @var int */ + public const ZEND_TRAIT_CONST2 = 321; + + public static int $staticTraitProp = 999; + + public function traitMethod2(): int {} + } + + class _ZendTestClassWithTraits + { + use _ZendTestTraitForInternalClass; + use _ZendTestTraitForInternalClass2; + } + /** @alias _ZendTestClassAlias */ class _ZendTestClass implements _ZendTestInterface { public const mixed TYPED_CLASS_CONST1 = []; diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index bd6548e7bffa..b2e342382db7 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 + * Stub hash: a221a3df3815679d61fd546ba120fd3a374fe71f * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, IS_NEVER, 0) @@ -226,6 +226,10 @@ ZEND_END_ARG_INFO() #define arginfo_ZendTestNS2_ZendSubNS_namespaced_deprecated_aliased_func arginfo_zend_test_void_return +#define arginfo_class__ZendTestTraitForInternalClass_traitMethod arginfo_zend_test_nodiscard + +#define arginfo_class__ZendTestTraitForInternalClass2_traitMethod2 arginfo_zend_test_nodiscard + #define arginfo_class__ZendTestClass_is_object arginfo_zend_test_nodiscard #define arginfo_class__ZendTestClass___toString arginfo_zend_get_current_func_name @@ -361,6 +365,8 @@ static ZEND_FUNCTION(ZendTestNS2_namespaced_func); static ZEND_FUNCTION(ZendTestNS2_namespaced_deprecated_func); static ZEND_FUNCTION(ZendTestNS2_ZendSubNS_namespaced_func); static ZEND_FUNCTION(ZendTestNS2_ZendSubNS_namespaced_deprecated_func); +static ZEND_METHOD(_ZendTestTraitForInternalClass, traitMethod); +static ZEND_METHOD(_ZendTestTraitForInternalClass2, traitMethod2); static ZEND_METHOD(_ZendTestClass, is_object); static ZEND_METHOD(_ZendTestClass, __toString); static ZEND_METHOD(_ZendTestClass, returnsStatic); @@ -542,6 +548,16 @@ static const zend_function_entry ext_functions[] = { ZEND_FE_END }; +static const zend_function_entry class__ZendTestTraitForInternalClass_methods[] = { + ZEND_ME(_ZendTestTraitForInternalClass, traitMethod, arginfo_class__ZendTestTraitForInternalClass_traitMethod, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class__ZendTestTraitForInternalClass2_methods[] = { + ZEND_ME(_ZendTestTraitForInternalClass2, traitMethod2, arginfo_class__ZendTestTraitForInternalClass2_traitMethod2, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static const zend_function_entry class__ZendTestClass_methods[] = { ZEND_ME(_ZendTestClass, is_object, arginfo_class__ZendTestClass_is_object, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(_ZendTestClass, __toString, arginfo_class__ZendTestClass___toString, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) @@ -720,6 +736,75 @@ static zend_class_entry *register_class__ZendTestInterface(void) return class_entry; } +static zend_class_entry *register_class__ZendTestTraitForInternalClass(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestTraitForInternalClass", class__ZendTestTraitForInternalClass_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_TRAIT); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_TRAIT; +#endif + + zval const_ZEND_TRAIT_CONST_value; + ZVAL_LONG(&const_ZEND_TRAIT_CONST_value, 123); + zend_string *const_ZEND_TRAIT_CONST_name = zend_string_init_interned("ZEND_TRAIT_CONST", sizeof("ZEND_TRAIT_CONST") - 1, true); + zend_declare_class_constant_ex(class_entry, const_ZEND_TRAIT_CONST_name, &const_ZEND_TRAIT_CONST_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release_ex(const_ZEND_TRAIT_CONST_name, true); + + zval property_traitProp_default_value; + ZVAL_LONG(&property_traitProp_default_value, 456); + zend_string *property_traitProp_name = zend_string_init("traitProp", sizeof("traitProp") - 1, true); + zend_declare_typed_property(class_entry, property_traitProp_name, &property_traitProp_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(property_traitProp_name, true); + + return class_entry; +} + +static zend_class_entry *register_class__ZendTestTraitForInternalClass2(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestTraitForInternalClass2", class__ZendTestTraitForInternalClass2_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_TRAIT); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_TRAIT; +#endif + + zval const_ZEND_TRAIT_CONST2_value; + ZVAL_LONG(&const_ZEND_TRAIT_CONST2_value, 321); + zend_string *const_ZEND_TRAIT_CONST2_name = zend_string_init_interned("ZEND_TRAIT_CONST2", sizeof("ZEND_TRAIT_CONST2") - 1, true); + zend_declare_class_constant_ex(class_entry, const_ZEND_TRAIT_CONST2_name, &const_ZEND_TRAIT_CONST2_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release_ex(const_ZEND_TRAIT_CONST2_name, true); + + zval property_staticTraitProp_default_value; + ZVAL_LONG(&property_staticTraitProp_default_value, 999); + zend_string *property_staticTraitProp_name = zend_string_init("staticTraitProp", sizeof("staticTraitProp") - 1, true); + zend_declare_typed_property(class_entry, property_staticTraitProp_name, &property_staticTraitProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(property_staticTraitProp_name, true); + + return class_entry; +} + +static zend_class_entry *register_class__ZendTestClassWithTraits(zend_class_entry *class_entry__ZendTestTraitForInternalClass, zend_class_entry *class_entry__ZendTestTraitForInternalClass2) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestClassWithTraits", NULL); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); +#endif + zend_class_use_internal_traits(class_entry, 2, class_entry__ZendTestTraitForInternalClass, class_entry__ZendTestTraitForInternalClass2); + + return class_entry; +} + static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_entry__ZendTestInterface) { zend_class_entry ce, *class_entry; diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index 2561000f4b60..ea6a2c94fbdf 100644 --- a/ext/zend_test/test_decl.h +++ b/ext/zend_test/test_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 */ + * Stub hash: a221a3df3815679d61fd546ba120fd3a374fe71f */ -#ifndef ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H -#define ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H +#ifndef ZEND_TEST_DECL_a221a3df3815679d61fd546ba120fd3a374fe71f_H +#define ZEND_TEST_DECL_a221a3df3815679d61fd546ba120fd3a374fe71f_H typedef enum zend_enum_ZendTestUnitEnum { ZEND_ENUM_ZendTestUnitEnum_Foo = 1, @@ -27,4 +27,4 @@ typedef enum zend_enum_ZendTestEnumWithInterface { ZEND_ENUM_ZendTestEnumWithInterface_Bar = 2, } zend_enum_ZendTestEnumWithInterface; -#endif /* ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H */ +#endif /* ZEND_TEST_DECL_a221a3df3815679d61fd546ba120fd3a374fe71f_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index a254a637e07d..05014c80fd5d 100644 --- a/ext/zend_test/test_legacy_arginfo.h +++ b/ext/zend_test/test_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 + * Stub hash: a221a3df3815679d61fd546ba120fd3a374fe71f * Has decl header: yes */ ZEND_BEGIN_ARG_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, 0) @@ -202,6 +202,10 @@ ZEND_END_ARG_INFO() #define arginfo_ZendTestNS2_ZendSubNS_namespaced_deprecated_aliased_func arginfo_zend_trigger_bailout +#define arginfo_class__ZendTestTraitForInternalClass_traitMethod arginfo_zend_trigger_bailout + +#define arginfo_class__ZendTestTraitForInternalClass2_traitMethod2 arginfo_zend_trigger_bailout + #define arginfo_class__ZendTestClass_is_object arginfo_zend_trigger_bailout #define arginfo_class__ZendTestClass___toString arginfo_zend_trigger_bailout @@ -324,6 +328,8 @@ static ZEND_FUNCTION(ZendTestNS2_namespaced_func); static ZEND_FUNCTION(ZendTestNS2_namespaced_deprecated_func); static ZEND_FUNCTION(ZendTestNS2_ZendSubNS_namespaced_func); static ZEND_FUNCTION(ZendTestNS2_ZendSubNS_namespaced_deprecated_func); +static ZEND_METHOD(_ZendTestTraitForInternalClass, traitMethod); +static ZEND_METHOD(_ZendTestTraitForInternalClass2, traitMethod2); static ZEND_METHOD(_ZendTestClass, is_object); static ZEND_METHOD(_ZendTestClass, __toString); static ZEND_METHOD(_ZendTestClass, returnsStatic); @@ -469,6 +475,16 @@ static const zend_function_entry ext_functions[] = { ZEND_FE_END }; +static const zend_function_entry class__ZendTestTraitForInternalClass_methods[] = { + ZEND_ME(_ZendTestTraitForInternalClass, traitMethod, arginfo_class__ZendTestTraitForInternalClass_traitMethod, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class__ZendTestTraitForInternalClass2_methods[] = { + ZEND_ME(_ZendTestTraitForInternalClass2, traitMethod2, arginfo_class__ZendTestTraitForInternalClass2_traitMethod2, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static const zend_function_entry class__ZendTestClass_methods[] = { ZEND_ME(_ZendTestClass, is_object, arginfo_class__ZendTestClass_is_object, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(_ZendTestClass, __toString, arginfo_class__ZendTestClass___toString, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) @@ -595,6 +611,75 @@ static zend_class_entry *register_class__ZendTestInterface(void) return class_entry; } +static zend_class_entry *register_class__ZendTestTraitForInternalClass(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestTraitForInternalClass", class__ZendTestTraitForInternalClass_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_TRAIT); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_TRAIT; +#endif + + zval const_ZEND_TRAIT_CONST_value; + ZVAL_LONG(&const_ZEND_TRAIT_CONST_value, 123); + zend_string *const_ZEND_TRAIT_CONST_name = zend_string_init_interned("ZEND_TRAIT_CONST", sizeof("ZEND_TRAIT_CONST") - 1, true); + zend_declare_class_constant_ex(class_entry, const_ZEND_TRAIT_CONST_name, &const_ZEND_TRAIT_CONST_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release_ex(const_ZEND_TRAIT_CONST_name, true); + + zval property_traitProp_default_value; + ZVAL_LONG(&property_traitProp_default_value, 456); + zend_string *property_traitProp_name = zend_string_init("traitProp", sizeof("traitProp") - 1, true); + zend_declare_property_ex(class_entry, property_traitProp_name, &property_traitProp_default_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release_ex(property_traitProp_name, true); + + return class_entry; +} + +static zend_class_entry *register_class__ZendTestTraitForInternalClass2(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestTraitForInternalClass2", class__ZendTestTraitForInternalClass2_methods); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_TRAIT); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); + class_entry->ce_flags |= ZEND_ACC_TRAIT; +#endif + + zval const_ZEND_TRAIT_CONST2_value; + ZVAL_LONG(&const_ZEND_TRAIT_CONST2_value, 321); + zend_string *const_ZEND_TRAIT_CONST2_name = zend_string_init_interned("ZEND_TRAIT_CONST2", sizeof("ZEND_TRAIT_CONST2") - 1, true); + zend_declare_class_constant_ex(class_entry, const_ZEND_TRAIT_CONST2_name, &const_ZEND_TRAIT_CONST2_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release_ex(const_ZEND_TRAIT_CONST2_name, true); + + zval property_staticTraitProp_default_value; + ZVAL_LONG(&property_staticTraitProp_default_value, 999); + zend_string *property_staticTraitProp_name = zend_string_init("staticTraitProp", sizeof("staticTraitProp") - 1, true); + zend_declare_property_ex(class_entry, property_staticTraitProp_name, &property_staticTraitProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_staticTraitProp_name, true); + + return class_entry; +} + +static zend_class_entry *register_class__ZendTestClassWithTraits(zend_class_entry *class_entry__ZendTestTraitForInternalClass, zend_class_entry *class_entry__ZendTestTraitForInternalClass2) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "_ZendTestClassWithTraits", NULL); +#if (PHP_VERSION_ID >= 80400) + class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); +#else + class_entry = zend_register_internal_class_ex(&ce, NULL); +#endif + zend_class_use_internal_traits(class_entry, 2, class_entry__ZendTestTraitForInternalClass, class_entry__ZendTestTraitForInternalClass2); + + return class_entry; +} + static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_entry__ZendTestInterface) { zend_class_entry ce, *class_entry; diff --git a/ext/zend_test/tests/internal_class_traits.phpt b/ext/zend_test/tests/internal_class_traits.phpt new file mode 100644 index 000000000000..c1a378ee99d4 --- /dev/null +++ b/ext/zend_test/tests/internal_class_traits.phpt @@ -0,0 +1,47 @@ +--TEST-- +Test traits in internal classes +--EXTENSIONS-- +zend_test +--FILE-- +traitProp); + +// Methods from both traits +var_dump($obj->traitMethod()); +var_dump($obj->traitMethod2()); + +// Static property from trait 2 +var_dump(_ZendTestClassWithTraits::$staticTraitProp); + +// Reflection should show both traits +$rc = new ReflectionClass(_ZendTestClassWithTraits::class); +$traits = $rc->getTraitNames(); +sort($traits); +var_dump(count($traits)); +var_dump($traits); + +echo "Done\n"; +?> +--EXPECT-- +int(123) +int(321) +int(456) +int(789) +int(101) +int(999) +int(2) +array(2) { + [0]=> + string(30) "_ZendTestTraitForInternalClass" + [1]=> + string(31) "_ZendTestTraitForInternalClass2" +} +Done From 56149186d81469fd7fb2f8444359ee81dd4a70f4 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 09:15:28 -0400 Subject: [PATCH 2/3] ext/filter: encode 0xFF in FILTER_SANITIZE_ENCODED php_filter_encode_url() initialized its 256-byte "must encode" table with memset(tmp, 1, sizeof(tmp) - 1), leaving tmp[255] uninitialized. Whether 0xFF got percent-encoded then depended on stack garbage; valgrind reports the read as a conditional jump on an uninitialised value. Initialize the whole table. Closes GH-22762 --- ext/filter/sanitizing_filters.c | 2 +- ext/filter/tests/filter_sanitize_encoded_0xff.phpt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 ext/filter/tests/filter_sanitize_encoded_0xff.phpt diff --git a/ext/filter/sanitizing_filters.c b/ext/filter/sanitizing_filters.c index 647d559c1df5..a356722765be 100644 --- a/ext/filter/sanitizing_filters.c +++ b/ext/filter/sanitizing_filters.c @@ -68,7 +68,7 @@ static void php_filter_encode_url(zval *value, const unsigned char* chars, const unsigned char *e = s + char_len; zend_string *str; - memset(tmp, 1, sizeof(tmp)-1); + memset(tmp, 1, sizeof(tmp)); while (s < e) { tmp[*s++] = '\0'; diff --git a/ext/filter/tests/filter_sanitize_encoded_0xff.phpt b/ext/filter/tests/filter_sanitize_encoded_0xff.phpt new file mode 100644 index 000000000000..1ee61aba7b70 --- /dev/null +++ b/ext/filter/tests/filter_sanitize_encoded_0xff.phpt @@ -0,0 +1,12 @@ +--TEST-- +FILTER_SANITIZE_ENCODED percent-encodes 0xFF +--EXTENSIONS-- +filter +--FILE-- + +--EXPECT-- +string(3) "%FF" +string(10) "%FE%FF%00A" From f6fe838e364943020daf802d8c9f55ef5099cf6f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 17 Jul 2026 12:20:39 +0100 Subject: [PATCH 3/3] ext/soap: truncate header values containing newline characters. Commit 3dcdb9826be routed the user_agent option through a truncate-and-warn check, but the Content-Type context option, the soapaction and the cookie names and values reach the same request buffer unchecked, so a CR/LF in any of them still injects arbitrary request headers. Emit all of them through that same check. The helper now takes an explicit length so the char* soapaction can share it. Close GH-22781 --- NEWS | 4 ++ ext/soap/php_http.c | 25 ++++---- .../tests/content_type_header_injection.phpt | 62 +++++++++++++++++++ ext/soap/tests/cookie_header_injection.phpt | 52 ++++++++++++++++ .../tests/soapaction_header_injection.phpt | 57 +++++++++++++++++ 5 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 ext/soap/tests/content_type_header_injection.phpt create mode 100644 ext/soap/tests/cookie_header_injection.phpt create mode 100644 ext/soap/tests/soapaction_header_injection.phpt diff --git a/NEWS b/NEWS index de11135f0d3b..fd5b2d859ee0 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,10 @@ PHP NEWS . Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes). (DanielEScherzer) +- SOAP: + . Fixed header injection through the Content-Type context option, the + soapaction and the cookie names and values. (David Carlier) + - Sockets: . Fixed socket_set_option() validation error messages for UDP_SEGMENT and TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du) diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index 22c18259d689..b49acd947b39 100644 --- a/ext/soap/php_http.c +++ b/ext/soap/php_http.c @@ -25,23 +25,24 @@ static zend_string *get_http_headers(php_stream *socketd); #define smart_str_append_const(str, const) \ smart_str_appendl(str,const,sizeof(const)-1) -static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name) +static void soap_smart_str_append_header_value_ex(smart_str *dest, const char *src, size_t len, const char *header_name) { - const char *src = ZSTR_VAL(value); - size_t len = ZSTR_LEN(value); size_t i = 0; while (i < len && src[i] != '\r' && src[i] != '\n') { i++; } + smart_str_appendl(dest, src, i); if (i < len) { - smart_str_appendl(dest, src, i); php_error_docref(NULL, E_WARNING, "Header %s value contains newline characters and has been truncated", header_name); - } else { - smart_str_append(dest, value); } } +static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name) +{ + soap_smart_str_append_header_value_ex(dest, ZSTR_VAL(value), ZSTR_LEN(value), header_name); +} + /* Proxy HTTP Authentication */ bool proxy_authentication(zval* this_ptr, smart_str* soap_headers) { @@ -656,13 +657,13 @@ bool make_http_soap_request( Z_STRLEN_P(tmp) > 0 ) { smart_str_append_const(&soap_headers, "Content-Type: "); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type"); } else { smart_str_append_const(&soap_headers, "Content-Type: application/soap+xml; charset=utf-8"); } if (soapaction) { smart_str_append_const(&soap_headers,"; action=\""); - smart_str_appends(&soap_headers, soapaction); + soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction"); smart_str_append_const(&soap_headers,"\""); } smart_str_append_const(&soap_headers,"\r\n"); @@ -673,14 +674,14 @@ bool make_http_soap_request( Z_STRLEN_P(tmp) > 0 ) { smart_str_append_const(&soap_headers, "Content-Type: "); - smart_str_append(&soap_headers, Z_STR_P(tmp)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type"); smart_str_append_const(&soap_headers, "\r\n"); } else { smart_str_append_const(&soap_headers, "Content-Type: text/xml; charset=utf-8\r\n"); } if (soapaction) { smart_str_append_const(&soap_headers, "SOAPAction: \""); - smart_str_appends(&soap_headers, soapaction); + soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction"); smart_str_append_const(&soap_headers, "\"\r\n"); } } @@ -892,9 +893,9 @@ bool make_http_soap_request( smart_str_appends(&soap_headers, "; "); } first_cookie = false; - smart_str_append(&soap_headers, key); + soap_smart_str_append_header_value(&soap_headers, key, "Cookie"); smart_str_appendc(&soap_headers, '='); - smart_str_append(&soap_headers, Z_STR_P(value)); + soap_smart_str_append_header_value(&soap_headers, Z_STR_P(value), "Cookie"); } } } diff --git a/ext/soap/tests/content_type_header_injection.phpt b/ext/soap/tests/content_type_header_injection.phpt new file mode 100644 index 000000000000..d04cf337164e --- /dev/null +++ b/ext/soap/tests/content_type_header_injection.phpt @@ -0,0 +1,62 @@ +--TEST-- +SoapClient must truncate a content_type context option that contains newline characters +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- + ['content_type' => "text/xml\r\nX-Injected: yes"], +]); + +foreach ([SOAP_1_1, SOAP_1_2] as $version) { + $client = new SoapClient(NULL, [ + 'location' => 'http://' . PHP_CLI_SERVER_ADDRESS, + 'uri' => 'misc-uri', + 'trace' => true, + 'soap_version' => $version, + 'stream_context' => $context, + ]); + + $client->__soapCall("foo", []); + echo $client->__getLastRequestHeaders(); +} + +?> +--EXPECTF-- +Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d +POST / HTTP/1.1 +Host: localhost:%d +Connection: Keep-Alive +User-Agent: PHP-SOAP/%s +Content-Type: text/xml +SOAPAction: "misc-uri#foo" +Content-Length: %d + + +Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d +POST / HTTP/1.1 +Host: localhost:%d +Connection: Keep-Alive +User-Agent: PHP-SOAP/%s +Content-Type: text/xml; action="misc-uri#foo" +Content-Length: %d diff --git a/ext/soap/tests/cookie_header_injection.phpt b/ext/soap/tests/cookie_header_injection.phpt new file mode 100644 index 000000000000..880050e84d03 --- /dev/null +++ b/ext/soap/tests/cookie_header_injection.phpt @@ -0,0 +1,52 @@ +--TEST-- +SoapClient must truncate a cookie name or value that contains newline characters +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- + 'http://' . PHP_CLI_SERVER_ADDRESS, + 'uri' => 'misc-uri', + 'trace' => true, +]); + +$client->__setCookie("evil-value", "a\r\nX-Injected: yes"); +$client->__setCookie("evil-name\r\nX-Injected: yes", "b"); +$client->__setCookie("harmless", "c"); + +$client->__soapCall("foo", []); +echo $client->__getLastRequestHeaders(); + +?> +--EXPECTF-- +Warning: SoapClient::__doRequest(): Header Cookie value contains newline characters and has been truncated in %s on line %d + +Warning: SoapClient::__doRequest(): Header Cookie value contains newline characters and has been truncated in %s on line %d +POST / HTTP/1.1 +Host: localhost:%d +Connection: Keep-Alive +User-Agent: PHP-SOAP/%s +Content-Type: text/xml; charset=utf-8 +SOAPAction: "misc-uri#foo" +Content-Length: %d +Cookie: evil-value=a; evil-name=b; harmless=c diff --git a/ext/soap/tests/soapaction_header_injection.phpt b/ext/soap/tests/soapaction_header_injection.phpt new file mode 100644 index 000000000000..996c193624f1 --- /dev/null +++ b/ext/soap/tests/soapaction_header_injection.phpt @@ -0,0 +1,57 @@ +--TEST-- +SoapClient must truncate a soapaction that contains newline characters +--EXTENSIONS-- +soap +--SKIPIF-- + +--FILE-- + 'http://' . PHP_CLI_SERVER_ADDRESS, + 'uri' => 'misc-uri', + 'trace' => true, + 'soap_version' => $version, + ]); + + $client->__soapCall("foo", [], ['soapaction' => "an-action\r\nX-Injected: yes"]); + echo $client->__getLastRequestHeaders(); +} + +?> +--EXPECTF-- +Warning: SoapClient::__doRequest(): Header SOAPAction value contains newline characters and has been truncated in %s on line %d +POST / HTTP/1.1 +Host: localhost:%d +Connection: Keep-Alive +User-Agent: PHP-SOAP/%s +Content-Type: text/xml; charset=utf-8 +SOAPAction: "an-action" +Content-Length: %d + + +Warning: SoapClient::__doRequest(): Header SOAPAction value contains newline characters and has been truncated in %s on line %d +POST / HTTP/1.1 +Host: localhost:%d +Connection: Keep-Alive +User-Agent: PHP-SOAP/%s +Content-Type: application/soap+xml; charset=utf-8; action="an-action" +Content-Length: %d