diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c
index e85b4ea42250..8df6a5599d30 100644
--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -3757,6 +3757,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 {
@@ -3764,6 +3769,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;
}
@@ -3784,6 +3790,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
diff --git a/ext/soap/soap.c b/ext/soap/soap.c
index 8a103e09636d..83584283740d 100644
--- a/ext/soap/soap.c
+++ b/ext/soap/soap.c
@@ -1414,7 +1414,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);
}
}
@@ -3179,6 +3179,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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+