From 90569082bbb446ab60e03541f3487731edb84342 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 15 Jun 2026 08:17:38 -0600 Subject: [PATCH 1/3] fix GH-20469: unsafe inheritance cache replay with reentrant autoloading (#22221) Inheritance cache dependencies are collected while a class is being linked. During delayed variance resolution, autoloading can re-enter class linking and use the current class while it is only nearly linked. If that class is persisted in the inheritance cache, a later request can replay dependencies in a different order and observe an incomplete hierarchy. When delayed autoloading causes the class to be used through the unlinked/nearly-linked lookup path, mark it as non-cacheable after load_delayed_classes() returns. This also catches cases where the class's variance obligations were resolved reentrantly, before the direct resolve_delayed_variance_obligations() call would run. If dependency tracking already allocated a temporary dependency table, free it when cache insertion is skipped. Restrict this cleanup to classes that entered inheritance-cache construction, because otherwise inheritance_cache is not a dependency table and may contain unrelated or uninitialized data. This preserves inheritance-cache use for delayed-variance classes that did not participate in this reentrant cycle. With the invalid cache entry prevented, unlinked_instanceof() can keep using instanceof_function() for linked classes. --- NEWS | 4 + Zend/zend_inheritance.c | 13 ++ ext/opcache/tests/gh20469.phpt | 134 +++++++++++++ ...h20469_child_variance_resolves_parent.phpt | 181 ++++++++++++++++++ .../gh20469_inheritance_cache_cleanup.phpt | 22 +++ .../tests/gh20469_inherited_method.phpt | 138 +++++++++++++ 6 files changed, 492 insertions(+) create mode 100644 ext/opcache/tests/gh20469.phpt create mode 100644 ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt create mode 100644 ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt create mode 100644 ext/opcache/tests/gh20469_inherited_method.phpt diff --git a/NEWS b/NEWS index 3ac671dda658..e0375b21d4fc 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,10 @@ PHP NEWS . Fix stmt->query leak in mysqli_execute_query() validation errors. (David Carlier) +- Opcache: + . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant + autoloading). (Levi Morrison) + - Phar: . Fixed a bypass of the magic ".phar" directory protection in Phar::addEmptyDir() for paths starting with "/.phar", while allowing diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index eba21dd8e82a..a4dd5f1893d5 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -3735,6 +3735,11 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { resolve_delayed_variance_obligations(ce); } + /* Delayed variance resolution can re-enter linking before the full + * hierarchy is linked. See ext/opcache/tests/gh20469*.phpt. */ + if (CG(unlinked_uses) && zend_hash_index_exists(CG(unlinked_uses), (zend_long)(uintptr_t) ce)) { + ce->ce_flags &= ~ZEND_ACC_CACHEABLE; + } if (ce->ce_flags & ZEND_ACC_CACHEABLE) { ce->ce_flags &= ~ZEND_ACC_CACHEABLE; } else { @@ -3742,6 +3747,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } + bool was_cacheable = is_cacheable; if (!CG(current_linking_class)) { is_cacheable = 0; } @@ -3762,6 +3768,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zend_hash_destroy(ht); FREE_HASHTABLE(ht); } + } else if (was_cacheable && ce->inheritance_cache) { + /* Cacheability can be disabled after dependency tracking prepared + * an inheritance-cache dependency table. Discard it here. */ + HashTable *ht = (HashTable*)ce->inheritance_cache; + ce->inheritance_cache = NULL; + zend_hash_destroy(ht); + FREE_HASHTABLE(ht); } if (!orig_record_errors) { diff --git a/ext/opcache/tests/gh20469.phpt b/ext/opcache/tests/gh20469.phpt new file mode 100644 index 000000000000..1cd826c177ef --- /dev/null +++ b/ext/opcache/tests/gh20469.phpt @@ -0,0 +1,134 @@ +--TEST-- +GH-20469: Inheritance cache with reentrant autoloading must not crash +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + ParentBeingLinked -> CovariantReturnWithTrait + * -> RequiresRootReturnTrait -> ChildOfParentBeingLinked. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3 diff --git a/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt b/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt new file mode 100644 index 000000000000..4a66c3b0513d --- /dev/null +++ b/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt @@ -0,0 +1,181 @@ +--TEST-- +GH-20469: Child delayed variance can resolve parent before direct delayed resolution +--DESCRIPTION-- +This variant ensures the cacheability check after load_delayed_classes() is +needed. Loading the delayed child resolves the parent class's variance +obligations reentrantly, so the parent no longer has ZEND_ACC_UNRESOLVED_VARIANCE +when control returns from load_delayed_classes(). The parent was still used while +nearly linked, and must not be inserted into the inheritance cache. +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + CovariantReturnWithTrait -> RequiresRootReturnTrait + * -> ChildOfParentBeingLinked -> ParentBeingLinked. + * + * ChildOfParentBeingLinked also has delayed variance, so resolving the child's + * dependency on ParentBeingLinked can resolve ParentBeingLinked before it + * reaches its direct resolve_delayed_variance_obligations() call. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/test3.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3NULL +3NULL diff --git a/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt b/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt new file mode 100644 index 000000000000..aabbc398cbc3 --- /dev/null +++ b/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-20469: Skipped inheritance cache cleanup must ignore non-cacheable classes +--DESCRIPTION-- +Autoloading the parent makes the child use the runtime class-linking path, but +the child does not enter inheritance-cache construction. Under ASAN, the +uninitialized inheritance_cache field is filled with non-zero bytes. Skipped +cache insertion must not treat that value as a temporary dependency table. +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/gh20469_inherited_method.phpt b/ext/opcache/tests/gh20469_inherited_method.phpt new file mode 100644 index 000000000000..f3c038bdc330 --- /dev/null +++ b/ext/opcache/tests/gh20469_inherited_method.phpt @@ -0,0 +1,138 @@ +--TEST-- +GH-20469: Inheritance cache with reentrant autoloading must preserve inherited methods +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + ParentBeingLinked -> CovariantReturnWithTrait + * -> RequiresRootReturnTrait -> ChildOfParentBeingLinked. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3NULL From 8a43a0aa7bc6ba294c07f7b2f7b008d64c1242c9 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 15 Jun 2026 08:20:04 -0600 Subject: [PATCH 2/3] Update NEWS for PR 22221 --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 1f4271f93cbe..235699b12bea 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,8 @@ PHP NEWS - Opcache: . Fixed bug GH-22265 (Another tailcall vm_interrupt bug). (Levi Morrison) + . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant + autoloading). (Levi Morrison) - Phar: . Fixed a bypass of the magic ".phar" directory protection in From a9987fa80e20249d57ba4d951a72c5b00056142f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 14 Jun 2026 13:44:18 +0100 Subject: [PATCH 3/3] ext/soap: ignore empty SOAPAction header during server dispatch. Fix #22285 Since 8.5 SoapServer::handle() uses the HTTP SOAPAction header to select the operation, falling back to the request body otherwise. An empty header made every request match the first WSDL operation. Skip empty SOAPAction headers so dispatch falls back to body inspection. close GH-22304 --- NEWS | 2 + ext/soap/soap.c | 6 +- ext/soap/tests/bugs/gh22285.phpt | 45 +++++++++++++++ ext/soap/tests/bugs/gh22285.wsdl | 96 ++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 ext/soap/tests/bugs/gh22285.phpt create mode 100644 ext/soap/tests/bugs/gh22285.wsdl diff --git a/NEWS b/NEWS index 235699b12bea..bd5752596004 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,8 @@ PHP NEWS - SOAP: . Fixed bug GH-22218 (SoapServer::handle() crash on $_SERVER not being an array). (David Carlier / Rex-Reynolds) + . Fixed bug GH-22285 (Soap server requires the raw input to be passed + to $server->handle). (David Carlier / ndossche) - Sqlite: . Fix error checks for column retrieval. (ndossche) diff --git a/ext/soap/soap.c b/ext/soap/soap.c index b7ed44929872..2bb49ee4c8fb 100644 --- a/ext/soap/soap.c +++ b/ext/soap/soap.c @@ -1394,7 +1394,7 @@ PHP_METHOD(SoapServer, handle) } } - if ((soap_action_z = zend_hash_str_find(Z_ARRVAL_P(server_vars), ZEND_STRL("HTTP_SOAPACTION"))) != NULL && Z_TYPE_P(soap_action_z) == IS_STRING) { + if ((soap_action_z = zend_hash_str_find(Z_ARRVAL_P(server_vars), ZEND_STRL("HTTP_SOAPACTION"))) != NULL && Z_TYPE_P(soap_action_z) == IS_STRING && Z_STRLEN_P(soap_action_z) > 0) { soap_action = Z_STRVAL_P(soap_action_z); } } @@ -3178,6 +3178,10 @@ static sdlFunctionPtr find_function_using_soap_action(const sdl *sdl, const char soap_action_length -= 2; } + if (UNEXPECTED(soap_action_length == 0)) { + return NULL; + } + /* TODO: This may depend on a particular target namespace, in which case this won't find a match when multiple different * target namespaces are used until #45282 is resolved. */ sdlFunctionPtr function; diff --git a/ext/soap/tests/bugs/gh22285.phpt b/ext/soap/tests/bugs/gh22285.phpt new file mode 100644 index 000000000000..8c7e0933588e --- /dev/null +++ b/ext/soap/tests/bugs/gh22285.phpt @@ -0,0 +1,45 @@ +--TEST-- +GH-22285 (SoapServer dispatches to the first function when the SOAPAction header is empty) +--CREDITS-- +Jarkko Hyvärinen +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--SKIPIF-- + +--POST-- + + + + World + + + +--FILE-- + 'Hello ' . $params->name]; + } + public function goodbye($params) { + return ['message' => 'Goodbye ' . $params->name]; + } +} + +$server = new SoapServer(__DIR__ . '/gh22285.wsdl', [ + 'cache_wsdl' => WSDL_CACHE_NONE, + 'encoding' => 'UTF-8', + 'soap_version' => SOAP_1_1, +]); +$server->setClass('TestWS'); +$_SERVER['HTTP_SOAPACTION'] = '""'; +$server->handle(); +?> +--EXPECTF-- + +Goodbye World diff --git a/ext/soap/tests/bugs/gh22285.wsdl b/ext/soap/tests/bugs/gh22285.wsdl new file mode 100644 index 000000000000..66b9e6749071 --- /dev/null +++ b/ext/soap/tests/bugs/gh22285.wsdl @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +