Repository navigation
Conversation
zend_jit_init_static_method_call() computes the called scope for a self:: or parent:: call from a non-static closure as a PHI of Z_OBJCE(EX(This)) and Z_CE(EX(This)), but discarded the PHI instead of assigning it to scope_ref. The callee then ran with the resolved class as called scope, so static::class and new static() returned Base instead of the forwarded Child (or failed with "Cannot instantiate abstract class"). Affects function and tracing JIT. Introduced by 972d6b6 (Fix JIT for INIT_STATIC_METHOD_CALL in a closure).
ndossche
approved these changes
Oct 13, 2026
ndossche
left a comment
Member
There was a problem hiding this comment.
Seems correct, thanks a lot!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24270
The bug
For an
INIT_STATIC_METHOD_CALLwithself::/parent::in a non-static closure,zend_jit_init_static_method_call()computes the called scope as a PHI ofZ_OBJCE(EX(This))(bound$this) andZ_CE(EX(This))(no$this), but the result ofir_PHI_list(values)was discarded instead of being assigned toscope_ref, as the two sibling branches do.scope_refkept the resolved class, so the callee ran with e.g.Baseas called scope instead of the forwardedChild:static::classreturns the wrong class, andnew static()creates the wrong class or throws "Cannot instantiate abstract class" (the case in the report).The code is shared by function and tracing JIT, so both are affected.
static fnclosures take theZEND_ACC_STATICbranch and are not affected. Introduced by 972d6b6 (Fix JIT for INIT_STATIC_METHOD_CALL in a closure), so PHP-8.5 and up; PHP-8.4 does not have this branch.The fix
Assign the PHI to
scope_ref.Tests
gh24270_001.phpt(function JIT) andgh24270_002.phpt(tracing JIT,jit_hot_func=1/jit_hot_loop=1) run the same cases fromgh24270.incand print the called scope seen by each closure over 100 calls:self::from a closure without$this(the report), with a bound$this, andparent::with a bound$this;self::andparent::from closures declared in a trait method, with the trait used by two classes with different parents.Each case is called through a subclass, so the expected called scope differs from both the closure's scope and the class the method is resolved in; a result taken from either of those fails the test.
The trait cases cover a different code shape: for a trait,
zend_get_known_class()returns the trait, the method lookup is skipped andfuncstays unknown, so function JIT emits the runtime static/non-static split, while tracing JIT takesfuncfrom the trace and emits a guard. The trait closure's opcodes are also shared by every using class, so the scope has to come fromEX(This)at runtime.Without the fix every case fails in both tests (function JIT:
Base/Mid/A/ParentAetc. on every call; tracing JIT: correct until the trace is compiled, then wrong).Verified on PHP-8.5 (bbfdd69), debug build, macOS arm64: both new tests fail without the fix and pass with it; the reproducer from the report prints
OKwith JIT off, function and tracing JIT.ext/opcache/tests(default INI) andZend/tests+ext/opcache/testsunder function and tracing JIT show no failures that are not also present without the change.Disclaimer
The root-cause analysis, the fix, the tests and this write-up were produced with Claude Opus 5.5 (Anthropic) at my direction; I ran the builds and tests on my machine and verified the results, but I cannot review the C myself. I will relay any review questions and apply requested changes.