Skip to content

fix GH-20469: unsafe inheritance cache replay with reentrant autoloading#22221

Open
morrisonlevi wants to merge 1 commit into
php:PHP-8.4from
morrisonlevi:crash-instanceof_function_slow
Open

fix GH-20469: unsafe inheritance cache replay with reentrant autoloading#22221
morrisonlevi wants to merge 1 commit into
php:PHP-8.4from
morrisonlevi:crash-instanceof_function_slow

Conversation

@morrisonlevi

@morrisonlevi morrisonlevi commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Symptoms and Background

Crashes commonly manifest in instanceof_function_slow. unlinked_instanceof() may see a linked starting class while an ancestor in its parent chain is still unlinked.

This has been a bug since PHP 8.1, I can reproduce it with the dockerhub php:8.1-cli image. I'm targeting PHP 8.4 for the fix, as it's the oldest version in active support.

Description

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 re-entrantly, 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 re-entrant cycle.

With the invalid cache entry prevented, unlinked_instanceof() can keep using instanceof_function() for linked classes.

@arnaud-lb

Copy link
Copy Markdown
Member

There may be a deeper issue as I think that we are supposed to mark classes as linked only when the fully hierarchy is linked. Especially, we shouldn't cache such class in the inheritance cache.

We can observe that linking is not complete as methods from grand parent of ChildOfParentBeingLinked are not inherited:

diff --git a/ext/opcache/tests/gh20469.phpt b/ext/opcache/tests/gh20469.phpt
index 1cd826c177e..dfab60c79be 100644
--- a/ext/opcache/tests/gh20469.phpt
+++ b/ext/opcache/tests/gh20469.phpt
@@ -33,6 +33,9 @@
 <?php
 require __DIR__ . '/autoload.php';
 echo \APP\ParentBeingLinked::SOME_CONSTANT;
+$i = new \APP\ChildOfParentBeingLinked();
+var_dump($i->test());
 PHP);
 
 file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP'
@@ -44,6 +47,7 @@ class RootForTraitReturn
     function createResult(): BaseCovariantReturn
     {
     }
+    function test() {}
 }
 PHP);

The call results in

Uncaught Error: Call to undefined method APP\ChildOfParentBeingLinked::test() in ext/opcache/tests/gh20469/test2.php:6

@bwoebi

bwoebi commented Jun 4, 2026

Copy link
Copy Markdown
Member

So, if I understand this correctly, the proper solution is excluding classes involved in a cycle during initial linking outside of opcache from is_cacheable?

It should be rather an edge case anyway and not have much impact thus to exclude these?

@morrisonlevi morrisonlevi force-pushed the crash-instanceof_function_slow branch from 9a1de97 to 2f0edf3 Compare June 11, 2026 17:01
@morrisonlevi morrisonlevi changed the title fix GH-20469: inheritance cache lookup with nested autoloading fix GH-20469: unsafe inheritance cache replay Jun 11, 2026
@morrisonlevi morrisonlevi changed the title fix GH-20469: unsafe inheritance cache replay fix GH-20469: unsafe inheritance cache replay with reentrant autoloading Jun 11, 2026
@morrisonlevi morrisonlevi force-pushed the crash-instanceof_function_slow branch from 2f0edf3 to 11b98f7 Compare June 11, 2026 17:03
@morrisonlevi

morrisonlevi commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

I've added more tests including the variant Arnaud showed, thanks! I force-pushed and brought it up to current for PHP 8.4, and redid the PR title and description to match.

Do you see any issues with the patch? Tests passed locally for me under ASAN, anyway (I did not run the full suite locally).

…oading

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.
@morrisonlevi morrisonlevi force-pushed the crash-instanceof_function_slow branch from 11b98f7 to 89d6398 Compare June 11, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants