From a96f6ff2c7a8452b78c1267f9e897ab6e2c5e849 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Fri, 31 Jul 2026 00:02:51 +0800 Subject: [PATCH 1/7] Fix GH-22935: Make php_hash.h includable from C++ code (#22940) --- ext/hash/php_hash.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index b77557c5bf78..bf99ce185b46 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -158,7 +158,7 @@ static inline void *php_hash_alloc_context(const php_hash_ops *ops) { /* Zero out context memory so serialization doesn't expose internals */ if (ops->context_align > 0) { size_t align = ops->context_align; - char *base = ecalloc(1, ops->context_size + align); + char *base = (char *) ecalloc(1, ops->context_size + align); size_t offset = align - ((uintptr_t)base & (align - 1)); char *ptr = base + offset; ptr[-1] = (char)offset; From 41f2ca4dd4eef3ed005adb7b1f074f0cc0a4f9ef Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Fri, 31 Jul 2026 00:18:12 +0800 Subject: [PATCH 2/7] ext/soap: Fix SOAP classmap validation for integer keys (#22884) Previously, SOAP only rejected packed arrays, which still allowed sparse integer-keyed arrays and mixed string integer-keyed arrays to be accepted. Since classmap is expected to be an associative mapping, arrays containing integer keys are now rejected consistently. Co-authored-by: David CARLIER Co-authored-by: NickSdot <32384907+NickSdot@users.noreply.github.com> --- NEWS | 3 + UPGRADING | 5 ++ ext/soap/php_encoding.c | 2 +- ext/soap/soap.c | 72 ++++++++++++------- ext/soap/tests/bugs/gh16256.phpt | 2 +- ext/soap/tests/classmap_invalid_keys.phpt | 68 ++++++++++++++++++ .../classmap_invalid_keys_error_types.phpt | 32 +++++++++ ...smap_invalid_keys_exceptions_disabled.phpt | 21 ++++++ ext/soap/tests/classmap_invalid_type.phpt | 30 ++++++++ ...lassmap_private_property_packed_array.phpt | 45 ++++++++++++ 10 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 ext/soap/tests/classmap_invalid_keys.phpt create mode 100644 ext/soap/tests/classmap_invalid_keys_error_types.phpt create mode 100644 ext/soap/tests/classmap_invalid_keys_exceptions_disabled.phpt create mode 100644 ext/soap/tests/classmap_invalid_type.phpt create mode 100644 ext/soap/tests/classmap_private_property_packed_array.phpt diff --git a/NEWS b/NEWS index d90af7f5e64d..ca60824033c1 100644 --- a/NEWS +++ b/NEWS @@ -79,6 +79,9 @@ PHP NEWS - SOAP: . Fixed header injection through the Content-Type context option, the soapaction and the cookie names and values. (David Carlier) + . Fixed the SoapClient and SoapServer "classmap" option to reject arrays + containing integer keys, and made SoapClient throw TypeError/ValueError + for invalid "classmap" options. (Weilin Du, David Carlier) - MBString: . Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative diff --git a/UPGRADING b/UPGRADING index b516f24db97f..8f107d19ec23 100644 --- a/UPGRADING +++ b/UPGRADING @@ -160,6 +160,11 @@ PHP 8.6 UPGRADE NOTES system. - SOAP: + . The "classmap" option of SoapClient and SoapServer now rejects arrays + containing integer keys. Previously, sparse integer-keyed and mixed-keyed + arrays could be accepted. SoapClient now throws TypeError for non-array + "classmap" options and ValueError for arrays containing integer keys, also + when the "exceptions" option is disabled. . WSDL/XML Schema parsing now rejects out-of-range integer values for occurrence constraints and integer restriction facets. Negative minOccurs and maxOccurs values are rejected as well. diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c index d08c5a683189..72de145e7b04 100644 --- a/ext/soap/php_encoding.c +++ b/ext/soap/php_encoding.c @@ -440,7 +440,7 @@ static xmlNodePtr master_to_xml_int(encodePtr encode, zval *data, int style, xml zval *tmp; zend_string *type_name; - ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(SOAP_GLOBAL(class_map), type_name, tmp) { + ZEND_HASH_FOREACH_STR_KEY_VAL(SOAP_GLOBAL(class_map), type_name, tmp) { ZVAL_DEREF(tmp); if (Z_TYPE_P(tmp) == IS_STRING && ZSTR_LEN(ce->name) == Z_STRLEN_P(tmp) && diff --git a/ext/soap/soap.c b/ext/soap/soap.c index 171b529b3355..4fade198d020 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -930,6 +930,18 @@ static HashTable* soap_create_typemap(sdlPtr sdl, HashTable *ht) /* {{{ */ } /* }}} */ +static bool soap_class_map_has_only_string_keys(const HashTable *class_map) +{ + zend_string *key; + ZEND_HASH_FOREACH_STR_KEY(class_map, key) { + if (UNEXPECTED(key == NULL)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + /* {{{ SoapServer constructor */ PHP_METHOD(SoapServer, __construct) { @@ -1007,8 +1019,7 @@ PHP_METHOD(SoapServer, __construct) zend_argument_type_error(2, "\"classmap\" option must be of type array, %s given", zend_zval_type_name(class_map_zv)); goto cleanup; } - // TODO: this still accepts mixed keys arrays and not all numerically indexed arrays are packed - if (UNEXPECTED(HT_IS_PACKED(Z_ARRVAL_P(class_map_zv)))) { + if (UNEXPECTED(!soap_class_map_has_only_string_keys(Z_ARRVAL_P(class_map_zv)))) { zend_argument_value_error(2, "\"classmap\" option must be an associative array"); goto cleanup; } @@ -2104,6 +2115,20 @@ PHP_METHOD(SoapClient, __construct) RETURN_THROWS(); } + if (options != NULL) { + zval *classmap = zend_hash_str_find(Z_ARRVAL_P(options), "classmap", sizeof("classmap")-1); + if (classmap != NULL) { + if (UNEXPECTED(Z_TYPE_P(classmap) != IS_ARRAY)) { + zend_argument_type_error(2, "\"classmap\" option must be of type array, %s given", zend_zval_type_name(classmap)); + RETURN_THROWS(); + } + if (UNEXPECTED(!soap_class_map_has_only_string_keys(Z_ARRVAL_P(classmap)))) { + zend_argument_value_error(2, "\"classmap\" option must be an associative array"); + RETURN_THROWS(); + } + } + } + SOAP_CLIENT_BEGIN_CODE(); cache_wsdl = SOAP_GLOBAL(cache_enabled) ? SOAP_GLOBAL(cache_mode) : 0; @@ -2134,14 +2159,6 @@ PHP_METHOD(SoapClient, __construct) } } - if ((tmp = zend_hash_str_find(ht, "stream_context", sizeof("stream_context")-1)) != NULL && - Z_TYPE_P(tmp) == IS_RESOURCE) { - context = php_stream_context_from_zval(tmp, 1); - Z_ADDREF_P(tmp); - } else { - context = php_stream_context_alloc(); - } - if ((tmp = zend_hash_str_find(ht, "location", sizeof("location")-1)) != NULL && Z_TYPE_P(tmp) == IS_STRING) { ZVAL_STR_COPY(Z_CLIENT_LOCATION_P(this_ptr), Z_STR_P(tmp)); @@ -2187,17 +2204,6 @@ PHP_METHOD(SoapClient, __construct) } } } - if ((tmp = zend_hash_str_find(ht, "local_cert", sizeof("local_cert")-1)) != NULL && - Z_TYPE_P(tmp) == IS_STRING) { - if (!context) { - context = php_stream_context_alloc(); - } - php_stream_context_set_option(context, "ssl", "local_cert", tmp); - if ((tmp = zend_hash_str_find(ht, "passphrase", sizeof("passphrase")-1)) != NULL && - Z_TYPE_P(tmp) == IS_STRING) { - php_stream_context_set_option(context, "ssl", "passphrase", tmp); - } - } if ((tmp = zend_hash_find(ht, ZSTR_KNOWN(ZEND_STR_TRACE))) != NULL && (Z_TYPE_P(tmp) == IS_TRUE || (Z_TYPE_P(tmp) == IS_LONG && Z_LVAL_P(tmp) == 1))) { @@ -2233,12 +2239,29 @@ PHP_METHOD(SoapClient, __construct) } if ((tmp = zend_hash_str_find(ht, "classmap", sizeof("classmap")-1)) != NULL && Z_TYPE_P(tmp) == IS_ARRAY) { - if (UNEXPECTED(HT_IS_PACKED(Z_ARRVAL_P(tmp)))) { - php_error_docref(NULL, E_ERROR, "'classmap' option must be an associative array"); - } ZVAL_COPY(Z_CLIENT_CLASSMAP_P(this_ptr), tmp); } + if ((tmp = zend_hash_str_find(ht, "stream_context", sizeof("stream_context")-1)) != NULL && + Z_TYPE_P(tmp) == IS_RESOURCE) { + context = php_stream_context_from_zval(tmp, 1); + Z_ADDREF_P(tmp); + } else { + context = php_stream_context_alloc(); + } + + if ((tmp = zend_hash_str_find(ht, "local_cert", sizeof("local_cert")-1)) != NULL && + Z_TYPE_P(tmp) == IS_STRING) { + if (!context) { + context = php_stream_context_alloc(); + } + php_stream_context_set_option(context, "ssl", "local_cert", tmp); + if ((tmp = zend_hash_str_find(ht, "passphrase", sizeof("passphrase")-1)) != NULL && + Z_TYPE_P(tmp) == IS_STRING) { + php_stream_context_set_option(context, "ssl", "passphrase", tmp); + } + } + if ((tmp = zend_hash_str_find(ht, "typemap", sizeof("typemap")-1)) != NULL && Z_TYPE_P(tmp) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(tmp)) > 0) { @@ -2313,6 +2336,7 @@ PHP_METHOD(SoapClient, __construct) if (typemap_ht) { soap_client_object_fetch(Z_OBJ_P(this_ptr))->typemap = soap_create_typemap(sdl, typemap_ht); } + SOAP_CLIENT_END_CODE(); } /* }}} */ diff --git a/ext/soap/tests/bugs/gh16256.phpt b/ext/soap/tests/bugs/gh16256.phpt index ce2ba1314069..a6d5f3fbbf3c 100644 --- a/ext/soap/tests/bugs/gh16256.phpt +++ b/ext/soap/tests/bugs/gh16256.phpt @@ -20,5 +20,5 @@ try { } ?> --EXPECT-- -SoapClient::__construct(): 'classmap' option must be an associative array +SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array diff --git a/ext/soap/tests/classmap_invalid_keys.phpt b/ext/soap/tests/classmap_invalid_keys.phpt new file mode 100644 index 000000000000..b30633dd0045 --- /dev/null +++ b/ext/soap/tests/classmap_invalid_keys.phpt @@ -0,0 +1,68 @@ +--TEST-- +SoapClient and SoapServer classmap options must only contain string keys +--EXTENSIONS-- +soap +--FILE-- + 'stdClass']; +unset($emptyHash['type']); + +$cases = [ + 'empty' => [], + 'empty hash' => $emptyHash, + 'packed' => ['stdClass'], + 'sparse numeric' => [100 => 'stdClass'], + 'numeric string' => ['1' => 'stdClass'], + 'mixed' => ['type' => 'stdClass', 1 => 'stdClass'], + 'associative' => ['type' => 'stdClass'], +]; + +foreach ($cases as $name => $classmap) { + echo "-- $name --\n"; + + try { + new SoapClient(null, [ + 'location' => 'http://example.com/', + 'uri' => 'urn:test', + 'classmap' => $classmap, + ]); + echo "SoapClient: OK\n"; + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + + try { + new SoapServer(null, [ + 'uri' => 'urn:test', + 'classmap' => $classmap, + ]); + echo "SoapServer: OK\n"; + } catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +?> +--EXPECT-- +-- empty -- +SoapClient: OK +SoapServer: OK +-- empty hash -- +SoapClient: OK +SoapServer: OK +-- packed -- +ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array +ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array +-- sparse numeric -- +ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array +ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array +-- numeric string -- +ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array +ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array +-- mixed -- +ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array +ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array +-- associative -- +SoapClient: OK +SoapServer: OK diff --git a/ext/soap/tests/classmap_invalid_keys_error_types.phpt b/ext/soap/tests/classmap_invalid_keys_error_types.phpt new file mode 100644 index 000000000000..57b73f09d72e --- /dev/null +++ b/ext/soap/tests/classmap_invalid_keys_error_types.phpt @@ -0,0 +1,32 @@ +--TEST-- +SoapClient and SoapServer report an invalid classmap option as ValueError +--EXTENSIONS-- +soap +--FILE-- + 'stdClass', 1 => 'stdClass']; + +try { + new SoapClient(null, [ + 'location' => 'http://example.com/', + 'uri' => 'urn:test', + 'classmap' => $classmap, + ]); +} catch (Throwable $e) { + echo 'SoapClient: ', $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + new SoapServer(null, [ + 'uri' => 'urn:test', + 'classmap' => $classmap, + ]); +} catch (Throwable $e) { + echo 'SoapServer: ', $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +SoapClient: ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array +SoapServer: ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array diff --git a/ext/soap/tests/classmap_invalid_keys_exceptions_disabled.phpt b/ext/soap/tests/classmap_invalid_keys_exceptions_disabled.phpt new file mode 100644 index 000000000000..8340a5dbf78c --- /dev/null +++ b/ext/soap/tests/classmap_invalid_keys_exceptions_disabled.phpt @@ -0,0 +1,21 @@ +--TEST-- +SoapClient reports an invalid classmap option as ValueError when exceptions are disabled +--EXTENSIONS-- +soap +--FILE-- + 'http://example.com/', + 'uri' => 'urn:test', + 'exceptions' => false, + 'classmap' => ['type' => 'stdClass', 1 => 'stdClass'], + ]); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array diff --git a/ext/soap/tests/classmap_invalid_type.phpt b/ext/soap/tests/classmap_invalid_type.phpt new file mode 100644 index 000000000000..2e6300aec8a5 --- /dev/null +++ b/ext/soap/tests/classmap_invalid_type.phpt @@ -0,0 +1,30 @@ +--TEST-- +SoapClient and SoapServer classmap options must be arrays +--EXTENSIONS-- +soap +--FILE-- + 'http://example.com/', + 'uri' => 'urn:test', + 'classmap' => 1, + ]); +} catch (Throwable $e) { + echo 'SoapClient: ', $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + new SoapServer(null, [ + 'uri' => 'urn:test', + 'classmap' => 1, + ]); +} catch (Throwable $e) { + echo 'SoapServer: ', $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +SoapClient: TypeError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be of type array, int given +SoapServer: TypeError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be of type array, int given diff --git a/ext/soap/tests/classmap_private_property_packed_array.phpt b/ext/soap/tests/classmap_private_property_packed_array.phpt new file mode 100644 index 000000000000..058483d2dc27 --- /dev/null +++ b/ext/soap/tests/classmap_private_property_packed_array.phpt @@ -0,0 +1,45 @@ +--TEST-- +SoapClient must not read packed private classmap as string-keyed map +--EXTENSIONS-- +soap +--FILE-- + + + + + SOAP-ENV:Server + expected fault + + + +XML; + } +} + +class Foo {} + +$client = new LocalSoapClient(null, [ + 'location' => 'http://example.org/', + 'uri' => 'http://example.org/', +]); + +$property = new ReflectionProperty(SoapClient::class, '_classmap'); +$property->setValue($client, ['Foo']); + +try { + $client->__soapCall('foo', [new Foo()]); +} catch (SoapFault) { + echo "SOAP Fault thrown\n"; +} + +?> +--EXPECT-- +__doRequest called +SOAP Fault thrown From 4782ec55aae78dd52e9e4af801c2144fdd5bf3e5 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 30 Jul 2026 12:47:52 -0400 Subject: [PATCH 3/7] Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, and tidyNode (#21694) These classes wrap native C handles (libxml2 writer/reader, SNMP session, libTidy document/node) that cannot survive serialization. Unserializing produces a broken object with NULL internal pointers, and tidyNode segfaults on use. bug72479.phpt exercised a UAF via unserialize() of SNMP; NOT_SERIALIZABLE rejects the class outright, closing that vector by construction. Fixes GH-21682 Closes GH-21694 --- ext/snmp/snmp.stub.php | 1 + ext/snmp/snmp_arginfo.h | 4 ++-- ext/snmp/tests/bug72479.phpt | 28 ++++++--------------------- ext/snmp/tests/gh21682.phpt | 17 ++++++++++++++++ ext/tidy/tests/gh21682.phpt | 26 +++++++++++++++++++++++++ ext/tidy/tidy.stub.php | 2 ++ ext/tidy/tidy_arginfo.h | 6 +++--- ext/xmlreader/php_xmlreader.stub.php | 1 + ext/xmlreader/php_xmlreader_arginfo.h | 4 ++-- ext/xmlreader/tests/gh21682.phpt | 16 +++++++++++++++ ext/xmlwriter/php_xmlwriter.stub.php | 1 + ext/xmlwriter/php_xmlwriter_arginfo.h | 4 ++-- ext/xmlwriter/tests/gh21682.phpt | 16 +++++++++++++++ 13 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 ext/snmp/tests/gh21682.phpt create mode 100644 ext/tidy/tests/gh21682.phpt create mode 100644 ext/xmlreader/tests/gh21682.phpt create mode 100644 ext/xmlwriter/tests/gh21682.phpt diff --git a/ext/snmp/snmp.stub.php b/ext/snmp/snmp.stub.php index 0a303aea77ff..0283f7ff7620 100644 --- a/ext/snmp/snmp.stub.php +++ b/ext/snmp/snmp.stub.php @@ -181,6 +181,7 @@ function snmp_get_valueretrieval(): int {} function snmp_read_mib(string $filename): bool {} +/** @not-serializable */ class SNMP { /** @cvalue SNMP_VERSION_1 */ diff --git a/ext/snmp/snmp_arginfo.h b/ext/snmp/snmp_arginfo.h index 1ee821f0538d..07caa70fa9eb 100644 --- a/ext/snmp/snmp_arginfo.h +++ b/ext/snmp/snmp_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit snmp.stub.php instead. - * Stub hash: e2451ac3ea0fa5eb1158e8b7252e61c6794d514f */ + * Stub hash: 20039fa88cb9f8a861bf3bf3e2e5e291e50c6a12 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_snmpget, 0, 3, IS_MIXED, 0) ZEND_ARG_TYPE_INFO(0, hostname, IS_STRING, 0) @@ -266,7 +266,7 @@ static zend_class_entry *register_class_SNMP(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "SNMP", class_SNMP_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE); zval const_VERSION_1_value; ZVAL_LONG(&const_VERSION_1_value, SNMP_VERSION_1); diff --git a/ext/snmp/tests/bug72479.phpt b/ext/snmp/tests/bug72479.phpt index 8127bbc94559..72c61905fc8f 100644 --- a/ext/snmp/tests/bug72479.phpt +++ b/ext/snmp/tests/bug72479.phpt @@ -10,28 +10,12 @@ require_once(__DIR__.'/skipif.inc'); >= 8; - } - return $out; +try { + $out = unserialize($poc); + var_dump($out); +} catch (Exception $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; } ?> --EXPECT-- -int(1) +Exception: Unserialization of 'SNMP' is not allowed diff --git a/ext/snmp/tests/gh21682.phpt b/ext/snmp/tests/gh21682.phpt new file mode 100644 index 000000000000..36b455035935 --- /dev/null +++ b/ext/snmp/tests/gh21682.phpt @@ -0,0 +1,17 @@ +--TEST-- +GH-21682 (SNMP should not be serializable) +--EXTENSIONS-- +snmp +--FILE-- +getMessage(), PHP_EOL; +} +$s->close(); +?> +--EXPECT-- +Exception: Serialization of 'SNMP' is not allowed diff --git a/ext/tidy/tests/gh21682.phpt b/ext/tidy/tests/gh21682.phpt new file mode 100644 index 000000000000..33ec0045961c --- /dev/null +++ b/ext/tidy/tests/gh21682.phpt @@ -0,0 +1,26 @@ +--TEST-- +GH-21682 (tidy and tidyNode should not be serializable) +--EXTENSIONS-- +tidy +--FILE-- +getMessage(), PHP_EOL; +} + +$t->parseString("test"); +$node = $t->body(); +try { + serialize($node); + echo "ERROR: should have thrown\n"; +} catch (\Exception $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} +?> +--EXPECT-- +Exception: Serialization of 'tidy' is not allowed +Exception: Serialization of 'tidyNode' is not allowed diff --git a/ext/tidy/tidy.stub.php b/ext/tidy/tidy.stub.php index add98c505b11..b3f142947fe2 100644 --- a/ext/tidy/tidy.stub.php +++ b/ext/tidy/tidy.stub.php @@ -861,6 +861,7 @@ function tidy_get_head(tidy $tidy): ?tidyNode {} function tidy_get_body(tidy $tidy): ?tidyNode {} +/** @not-serializable */ class tidy { public ?string $errorBuffer = null; @@ -973,6 +974,7 @@ public function html(): ?tidyNode {} public function body(): ?tidyNode {} } +/** @not-serializable */ final class tidyNode { public readonly string $value; diff --git a/ext/tidy/tidy_arginfo.h b/ext/tidy/tidy_arginfo.h index 22336502bfd5..cded60957021 100644 --- a/ext/tidy/tidy_arginfo.h +++ b/ext/tidy/tidy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit tidy.stub.php instead. - * Stub hash: 0e6561410a63658f76011c1ddcecdd1e68757f0a */ + * Stub hash: 7a1ba6bc8ec95e846ec89060b30f54d2c32486ef */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_tidy_parse_string, 0, 1, tidy, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) @@ -472,7 +472,7 @@ static zend_class_entry *register_class_tidy(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "tidy", class_tidy_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE); zval property_errorBuffer_default_value; ZVAL_NULL(&property_errorBuffer_default_value); @@ -492,7 +492,7 @@ static zend_class_entry *register_class_tidyNode(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "tidyNode", class_tidyNode_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NOT_SERIALIZABLE); zval property_value_default_value; ZVAL_UNDEF(&property_value_default_value); diff --git a/ext/xmlreader/php_xmlreader.stub.php b/ext/xmlreader/php_xmlreader.stub.php index d31903706604..d2ce48c43f35 100644 --- a/ext/xmlreader/php_xmlreader.stub.php +++ b/ext/xmlreader/php_xmlreader.stub.php @@ -2,6 +2,7 @@ /** @generate-class-entries */ +/** @not-serializable */ class XMLReader { /* Constants for NodeType - cannot define common types to share with dom as there are differences in these types */ diff --git a/ext/xmlreader/php_xmlreader_arginfo.h b/ext/xmlreader/php_xmlreader_arginfo.h index f0950020c8a5..8283a72f7d30 100644 --- a/ext/xmlreader/php_xmlreader_arginfo.h +++ b/ext/xmlreader/php_xmlreader_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_xmlreader.stub.php instead. - * Stub hash: 80288a0f40eabc7802a928963386616ea31e448d */ + * Stub hash: 11cf6e4c523d9ebbe2775d5bd127be303402336f */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLReader_close, 0, 0, IS_TRUE, 0) ZEND_END_ARG_INFO() @@ -176,7 +176,7 @@ static zend_class_entry *register_class_XMLReader(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "XMLReader", class_XMLReader_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE); zval const_NONE_value; ZVAL_LONG(&const_NONE_value, XML_READER_TYPE_NONE); diff --git a/ext/xmlreader/tests/gh21682.phpt b/ext/xmlreader/tests/gh21682.phpt new file mode 100644 index 000000000000..95b8dd222d3b --- /dev/null +++ b/ext/xmlreader/tests/gh21682.phpt @@ -0,0 +1,16 @@ +--TEST-- +GH-21682 (XMLReader should not be serializable) +--EXTENSIONS-- +xmlreader +--FILE-- +getMessage(), PHP_EOL; +} +?> +--EXPECT-- +Exception: Serialization of 'XMLReader' is not allowed diff --git a/ext/xmlwriter/php_xmlwriter.stub.php b/ext/xmlwriter/php_xmlwriter.stub.php index 44b509aa1dd7..3d002d5c87c1 100644 --- a/ext/xmlwriter/php_xmlwriter.stub.php +++ b/ext/xmlwriter/php_xmlwriter.stub.php @@ -86,6 +86,7 @@ function xmlwriter_output_memory(XMLWriter $writer, bool $flush = true): string function xmlwriter_flush(XMLWriter $writer, bool $empty = true): string|int {} +/** @not-serializable */ class XMLWriter { /** diff --git a/ext/xmlwriter/php_xmlwriter_arginfo.h b/ext/xmlwriter/php_xmlwriter_arginfo.h index 8170077bdab0..33e85c3e9ec5 100644 --- a/ext/xmlwriter/php_xmlwriter_arginfo.h +++ b/ext/xmlwriter/php_xmlwriter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_xmlwriter.stub.php instead. - * Stub hash: fcc388de55bd6d21530d16f6a9ab5f0eb307c1ff */ + * Stub hash: 27ddfb10eeeda8acfa744751fdcdd030a207f899 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_xmlwriter_open_uri, 0, 1, XMLWriter, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0) @@ -484,7 +484,7 @@ static zend_class_entry *register_class_XMLWriter(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "XMLWriter", class_XMLWriter_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE); return class_entry; } diff --git a/ext/xmlwriter/tests/gh21682.phpt b/ext/xmlwriter/tests/gh21682.phpt new file mode 100644 index 000000000000..8437301a7cff --- /dev/null +++ b/ext/xmlwriter/tests/gh21682.phpt @@ -0,0 +1,16 @@ +--TEST-- +GH-21682 (XMLWriter should not be serializable) +--EXTENSIONS-- +xmlwriter +--FILE-- +getMessage(), PHP_EOL; +} +?> +--EXPECT-- +Exception: Serialization of 'XMLWriter' is not allowed From e461e70c70d5ef21764aac9b352fac6fafbbe9f8 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 19:10:18 +0100 Subject: [PATCH 4/7] streams: check class exists when attempting to register stream filter (#22823) And do a few other checks such as is the class even instantiable. It might make sense in the future to raise a deprecation if the class does not extend php_user_filter or create a new StreamFilter interface so that we know the various methods exist --- ext/standard/tests/filters/001.phpt | 27 ++++----- ext/standard/tests/filters/gh17037.phpt | 8 ++- ...am_filter_register_non_existing_class.phpt | 12 ++-- ext/standard/user_filters.c | 58 +++++-------------- 4 files changed, 42 insertions(+), 63 deletions(-) diff --git a/ext/standard/tests/filters/001.phpt b/ext/standard/tests/filters/001.phpt index 3f0d5f5eb44e..0a91b7213445 100644 --- a/ext/standard/tests/filters/001.phpt +++ b/ext/standard/tests/filters/001.phpt @@ -3,30 +3,27 @@ stream_filter_register() and invalid arguments --FILE-- getMessage() . "\n"; + stream_filter_register("", "stdClass"); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { - stream_filter_register("test", ""); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; + stream_filter_register("test", "Throwable"); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } try { - stream_filter_register("", "test"); -} catch (ValueError $exception) { - echo $exception->getMessage() . "\n"; + var_dump(stream_filter_register("------", "nonexistentclass")); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } -var_dump(stream_filter_register("------", "nonexistentclass")); - echo "Done\n"; ?> --EXPECT-- -stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string -stream_filter_register(): Argument #2 ($class) must be a non-empty string -stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string -bool(true) +ValueError: stream_filter_register(): Argument #1 ($filter_name) must be a non-empty string +ValueError: stream_filter_register(): Argument #2 ($class) must be a concrete class +TypeError: stream_filter_register(): Argument #2 ($class) must be a valid class name, nonexistentclass given Done diff --git a/ext/standard/tests/filters/gh17037.phpt b/ext/standard/tests/filters/gh17037.phpt index 21319ba26bf9..c6124c0fc2ba 100644 --- a/ext/standard/tests/filters/gh17037.phpt +++ b/ext/standard/tests/filters/gh17037.phpt @@ -2,7 +2,13 @@ GH-17037 (UAF in user filter when adding existing filter name due to incorrect error handling) --FILE-- getMessage(), PHP_EOL; +} ?> --EXPECT-- bool(false) diff --git a/ext/standard/tests/filters/stream_filter_register_non_existing_class.phpt b/ext/standard/tests/filters/stream_filter_register_non_existing_class.phpt index 1c08201cff0c..b404d45524b6 100644 --- a/ext/standard/tests/filters/stream_filter_register_non_existing_class.phpt +++ b/ext/standard/tests/filters/stream_filter_register_non_existing_class.phpt @@ -3,7 +3,11 @@ stream_filter_register() with a class name that does not exist --FILE-- getMessage(), PHP_EOL; +} stream_filter_append(STDOUT, "not_existing_filter"); @@ -12,10 +16,8 @@ var_dump($out); ?> --EXPECTF-- -bool(true) +TypeError: stream_filter_register(): Argument #2 ($class) must be a valid class name, not_existing given -Warning: stream_filter_append(): User-filter "not_existing_filter" requires class "not_existing", but that class is not defined in %s on line %d - -Warning: stream_filter_append(): Unable to create or locate filter "not_existing_filter" in %s on line %d +Warning: stream_filter_append(): Unable to locate filter "not_existing_filter" in %s on line %d Hello int(6) diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 126b434713ad..9711ad1f83f2 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -23,12 +23,6 @@ #define PHP_STREAM_BRIGADE_RES_NAME "userfilter.bucket brigade" #define PHP_STREAM_BUCKET_RES_NAME "userfilter.bucket" -struct php_user_filter_data { - zend_class_entry *ce; - /* variable length; this *must* be last in the structure */ - zend_string *classname; -}; - /* to provide context for calling into the next filter from user-space */ static int le_bucket_brigade; static int le_bucket; @@ -328,7 +322,6 @@ static const php_stream_filter_ops userfilter_ops = { static php_stream_filter *user_filter_factory_create(const char *filtername, zval *filterparams, bool persistent) { - struct php_user_filter_data *fdat = NULL; php_stream_filter *filter; zval obj; zval retval; @@ -347,8 +340,9 @@ static php_stream_filter *user_filter_factory_create(const char *filtername, len = strlen(filtername); - /* determine the classname/class entry */ - if (NULL == (fdat = zend_hash_str_find_ptr(BG(user_filter_map), filtername, len))) { + /* determine the class entry */ + /* const */ zend_class_entry *ce = zend_hash_str_find_ptr(BG(user_filter_map), filtername, len); + if (UNEXPECTED(ce == NULL)) { const char *period; /* Userspace Filters using ambiguous wildcards could cause problems. @@ -366,7 +360,8 @@ static php_stream_filter *user_filter_factory_create(const char *filtername, ZEND_ASSERT(new_period[0] == '.'); new_period[1] = '*'; new_period[2] = '\0'; - if (NULL != (fdat = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard)))) { + ce = zend_hash_str_find_ptr(BG(user_filter_map), wildcard, strlen(wildcard)); + if (NULL != ce) { new_period = NULL; } else { *new_period = '\0'; @@ -375,21 +370,11 @@ static php_stream_filter *user_filter_factory_create(const char *filtername, } efree(wildcard); } - ZEND_ASSERT(fdat); - } - - /* bind the classname to the actual class */ - if (fdat->ce == NULL) { - if (NULL == (fdat->ce = zend_lookup_class(fdat->classname))) { - php_error_docref(NULL, E_WARNING, - "User-filter \"%s\" requires class \"%s\", but that class is not defined", - filtername, ZSTR_VAL(fdat->classname)); - return NULL; - } + ZEND_ASSERT(ce); } /* create the object */ - if (object_init_ex(&obj, fdat->ce) == FAILURE) { + if (object_init_ex(&obj, ce) == FAILURE) { return NULL; } @@ -435,13 +420,6 @@ static const php_stream_filter_factory user_filter_factory = { user_filter_factory_create }; -static void filter_item_dtor(zval *zv) -{ - struct php_user_filter_data *fdat = Z_PTR_P(zv); - zend_string_release_ex(fdat->classname, 0); - efree(fdat); -} - /* {{{ Return a bucket object from the brigade for operating on */ PHP_FUNCTION(stream_bucket_make_writeable) { @@ -604,12 +582,12 @@ PHP_FUNCTION(stream_get_filters) /* {{{ Registers a custom filter handler class */ PHP_FUNCTION(stream_filter_register) { - zend_string *filtername, *classname; - struct php_user_filter_data *fdat; + zend_string *filtername; + zend_class_entry *ce = NULL; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_STR(filtername) - Z_PARAM_STR(classname) + Z_PARAM_CLASS(ce) ZEND_PARSE_PARAMETERS_END(); if (!ZSTR_LEN(filtername)) { @@ -617,28 +595,24 @@ PHP_FUNCTION(stream_filter_register) RETURN_THROWS(); } - if (!ZSTR_LEN(classname)) { - zend_argument_value_error(2, "must be a non-empty string"); + /* TODO: Check class is a child of php_user_filter? */ + if (UNEXPECTED(ce->ce_flags & ZEND_ACC_UNINSTANTIABLE)) { + zend_argument_value_error(2, "must be a concrete class"); RETURN_THROWS(); } if (!BG(user_filter_map)) { BG(user_filter_map) = (HashTable*) emalloc(sizeof(HashTable)); - zend_hash_init(BG(user_filter_map), 8, NULL, (dtor_func_t) filter_item_dtor, 0); + /* We don't need a destructor as we are only storing a CE which should be never modified */ + zend_hash_init(BG(user_filter_map), 8, NULL, NULL, 0); } - fdat = ecalloc(1, sizeof(struct php_user_filter_data)); - fdat->classname = zend_string_copy(classname); - - if (zend_hash_add_ptr(BG(user_filter_map), filtername, fdat) != NULL) { + if (zend_hash_add_ptr(BG(user_filter_map), filtername, ce) != NULL) { if (php_stream_filter_register_factory_volatile(filtername, &user_filter_factory) == SUCCESS) { RETURN_TRUE; } zend_hash_del(BG(user_filter_map), filtername); - } else { - zend_string_release_ex(classname, 0); - efree(fdat); } RETURN_FALSE; From ee019fab5a2d3e5dbf8eb3c3613f04fb8a09b2fa Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 26 Jun 2026 11:40:10 +0100 Subject: [PATCH 5/7] ext/reflection: move ReflectionAttribute tests into subfolder --- .../{ => attributes}/ReflectionAttribute_constructor_001.phpt | 0 .../ReflectionAttribute_newInstance_deprecated.phpt | 0 .../ReflectionAttribute_newInstance_exception.phpt | 0 .../tests/{ => attributes}/ReflectionAttribute_toString.phpt | 0 ext/reflection/tests/{ => attributes}/gh12908.phpt | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename ext/reflection/tests/{ => attributes}/ReflectionAttribute_constructor_001.phpt (100%) rename ext/reflection/tests/{ => attributes}/ReflectionAttribute_newInstance_deprecated.phpt (100%) rename ext/reflection/tests/{ => attributes}/ReflectionAttribute_newInstance_exception.phpt (100%) rename ext/reflection/tests/{ => attributes}/ReflectionAttribute_toString.phpt (100%) rename ext/reflection/tests/{ => attributes}/gh12908.phpt (100%) diff --git a/ext/reflection/tests/ReflectionAttribute_constructor_001.phpt b/ext/reflection/tests/attributes/ReflectionAttribute_constructor_001.phpt similarity index 100% rename from ext/reflection/tests/ReflectionAttribute_constructor_001.phpt rename to ext/reflection/tests/attributes/ReflectionAttribute_constructor_001.phpt diff --git a/ext/reflection/tests/ReflectionAttribute_newInstance_deprecated.phpt b/ext/reflection/tests/attributes/ReflectionAttribute_newInstance_deprecated.phpt similarity index 100% rename from ext/reflection/tests/ReflectionAttribute_newInstance_deprecated.phpt rename to ext/reflection/tests/attributes/ReflectionAttribute_newInstance_deprecated.phpt diff --git a/ext/reflection/tests/ReflectionAttribute_newInstance_exception.phpt b/ext/reflection/tests/attributes/ReflectionAttribute_newInstance_exception.phpt similarity index 100% rename from ext/reflection/tests/ReflectionAttribute_newInstance_exception.phpt rename to ext/reflection/tests/attributes/ReflectionAttribute_newInstance_exception.phpt diff --git a/ext/reflection/tests/ReflectionAttribute_toString.phpt b/ext/reflection/tests/attributes/ReflectionAttribute_toString.phpt similarity index 100% rename from ext/reflection/tests/ReflectionAttribute_toString.phpt rename to ext/reflection/tests/attributes/ReflectionAttribute_toString.phpt diff --git a/ext/reflection/tests/gh12908.phpt b/ext/reflection/tests/attributes/gh12908.phpt similarity index 100% rename from ext/reflection/tests/gh12908.phpt rename to ext/reflection/tests/attributes/gh12908.phpt From 9e9445f7421c7a55e65305f3887995a54d3da410 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 26 Jun 2026 11:38:41 +0100 Subject: [PATCH 6/7] ext/reflection: add namespace methods to ReflectionAttribute Add the ReflectionAttribute::inNamespace(), ReflectionAttribute::getNamespaceName(), and ReflectionAttribute::getShortName() methods. Closes GH-19556 --- ext/reflection/php_reflection.c | 52 +++++++++++++++++++ ext/reflection/php_reflection.stub.php | 3 ++ ext/reflection/php_reflection_arginfo.h | 14 ++++- ext/reflection/php_reflection_decl.h | 8 +-- ...ctionAttribute_name_namespace_methods.phpt | 52 +++++++++++++++++++ 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 ext/reflection/tests/attributes/ReflectionAttribute_name_namespace_methods.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index ec060cbbf3e9..7fa76c121cac 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7370,6 +7370,58 @@ ZEND_METHOD(ReflectionAttribute, getName) } /* }}} */ +/* Returns whether this attribute name comes from a namespace */ +ZEND_METHOD(ReflectionAttribute, inNamespace) +{ + reflection_object *intern; + attribute_reference *attr; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(attr); + + const zend_string *name = attr->data->name; + const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + RETURN_BOOL(backslash); +} +/* }}} */ + +/* Returns the name of namespace where this attribute comes from */ +ZEND_METHOD(ReflectionAttribute, getNamespaceName) +{ + reflection_object *intern; + attribute_reference *attr; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(attr); + + const zend_string *name = attr->data->name; + const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + if (backslash) { + RETURN_STRINGL(ZSTR_VAL(name), backslash - ZSTR_VAL(name)); + } + RETURN_EMPTY_STRING(); +} + +/* Returns the short name of the attribute (without namespace part) */ +ZEND_METHOD(ReflectionAttribute, getShortName) +{ + reflection_object *intern; + attribute_reference *attr; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(attr); + + zend_string *name = attr->data->name; + const char *backslash = zend_memrchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + if (backslash) { + RETURN_STRINGL(backslash + 1, ZSTR_LEN(name) - (backslash - ZSTR_VAL(name) + 1)); + } + RETURN_STR_COPY(name); +} + /* {{{ Returns the target of the attribute */ ZEND_METHOD(ReflectionAttribute, getTarget) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..b95f01d2ad28 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -849,6 +849,9 @@ class ReflectionAttribute implements Reflector public string $name; public function getName(): string {} + public function inNamespace(): bool {} + public function getNamespaceName(): string {} + public function getShortName(): string {} public function getTarget(): int {} public function isRepeated(): bool {} public function getArguments(): array {} diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..7da379d7b989 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 + * Stub hash: c4dcc2653f826c2c437065faec4bf77772ef88b1 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -646,6 +646,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionAttribute_getName arginfo_class_ReflectionFunction___toString +#define arginfo_class_ReflectionAttribute_inNamespace arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + +#define arginfo_class_ReflectionAttribute_getNamespaceName arginfo_class_ReflectionFunction___toString + +#define arginfo_class_ReflectionAttribute_getShortName arginfo_class_ReflectionFunction___toString + #define arginfo_class_ReflectionAttribute_getTarget arginfo_class_ReflectionGenerator_getExecutingLine #define arginfo_class_ReflectionAttribute_isRepeated arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType @@ -974,6 +980,9 @@ ZEND_METHOD(ReflectionReference, fromArrayElement); ZEND_METHOD(ReflectionReference, getId); ZEND_METHOD(ReflectionReference, __construct); ZEND_METHOD(ReflectionAttribute, getName); +ZEND_METHOD(ReflectionAttribute, inNamespace); +ZEND_METHOD(ReflectionAttribute, getNamespaceName); +ZEND_METHOD(ReflectionAttribute, getShortName); ZEND_METHOD(ReflectionAttribute, getTarget); ZEND_METHOD(ReflectionAttribute, isRepeated); ZEND_METHOD(ReflectionAttribute, getArguments); @@ -1327,6 +1336,9 @@ static const zend_function_entry class_ReflectionReference_methods[] = { static const zend_function_entry class_ReflectionAttribute_methods[] = { ZEND_ME(ReflectionAttribute, getName, arginfo_class_ReflectionAttribute_getName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionAttribute, inNamespace, arginfo_class_ReflectionAttribute_inNamespace, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionAttribute, getNamespaceName, arginfo_class_ReflectionAttribute_getNamespaceName, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionAttribute, getShortName, arginfo_class_ReflectionAttribute_getShortName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionAttribute, getTarget, arginfo_class_ReflectionAttribute_getTarget, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionAttribute, isRepeated, arginfo_class_ReflectionAttribute_isRepeated, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionAttribute, getArguments, arginfo_class_ReflectionAttribute_getArguments, ZEND_ACC_PUBLIC) diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..555ed84feebe 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,12 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: c4dcc2653f826c2c437065faec4bf77772ef88b1 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_c4dcc2653f826c2c437065faec4bf77772ef88b1_H +#define ZEND_PHP_REFLECTION_DECL_c4dcc2653f826c2c437065faec4bf77772ef88b1_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H */ +#endif /* ZEND_PHP_REFLECTION_DECL_c4dcc2653f826c2c437065faec4bf77772ef88b1_H */ diff --git a/ext/reflection/tests/attributes/ReflectionAttribute_name_namespace_methods.phpt b/ext/reflection/tests/attributes/ReflectionAttribute_name_namespace_methods.phpt new file mode 100644 index 000000000000..9f465e3c05cd --- /dev/null +++ b/ext/reflection/tests/attributes/ReflectionAttribute_name_namespace_methods.phpt @@ -0,0 +1,52 @@ +--TEST-- +ReflectionAttribute name and namespace methods +--FILE-- +getAttributes()[0]; + + var_dump($attribute->inNamespace()); + var_dump($attribute->getName()); + var_dump($attribute->getNamespaceName()); + var_dump($attribute->getShortName()); + + $rm = new \ReflectionMethod(Foo::class, "fn2"); + $attribute = $rm->getAttributes()[0]; + + var_dump($attribute->inNamespace()); + var_dump($attribute->getName()); + var_dump($attribute->getNamespaceName()); + var_dump($attribute->getShortName()); +} + +?> +--EXPECT-- +bool(false) +string(4) "Attr" +string(0) "" +string(4) "Attr" +bool(true) +string(17) "A\B\AttrNamespace" +string(3) "A\B" +string(13) "AttrNamespace" From 9ed85af67e9a90d1599b9880720f23c2e7e06eae Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 21:18:10 +0100 Subject: [PATCH 7/7] NEWS/UPGRADING entries for new ReflectionAttribute methods --- NEWS | 5 +++++ UPGRADING | 3 +++ 2 files changed, 8 insertions(+) diff --git a/NEWS b/NEWS index ca60824033c1..4364d69650e5 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,11 @@ PHP NEWS error handler instead of emitting a warning and continuing with an empty string. (Weilin Du) +- Reflection: + . Added ReflectionAttribute::inNamespace(), + ReflectionAttribute::getNamespaceName(), and + ReflectionAttribute::getShortName(). (Girgias) + - Standard: . The following functions now raise a ValueError when the $filename argument contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(), diff --git a/UPGRADING b/UPGRADING index 8f107d19ec23..fd66c7e61180 100644 --- a/UPGRADING +++ b/UPGRADING @@ -483,6 +483,9 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/isreadable-iswriteable . ReflectionParameter::getDocComment() RFC: https://wiki.php.net/rfc/parameter-doccomments + . ReflectionAttribute::inNamespace() + . ReflectionAttribute::getNamespaceName() + . ReflectionAttribute::getShortName() - Standard: . clamp() returns the given value if in range, else return the nearest