Commit 4c3d44638eb for php
commit 4c3d44638eb7eaaea1526452e5d8e7176b243ead
Author: RV7PR <17615589+RV7PR@users.noreply.github.com>
Date: Tue Oct 13 07:48:25 2026 +0200
Fix GH-24270: JIT loses called scope of self::/parent:: in closures
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 972d6b6664e (Fix JIT for INIT_STATIC_METHOD_CALL in a
closure).
Closes GH-24287.
diff --git a/NEWS b/NEWS
index a8b32cd0d65..95fff33d289 100644
--- a/NEWS
+++ b/NEWS
@@ -41,6 +41,8 @@ PHP NEWS
. Fix too wide type inference for ASSIGN_DIM_OP. (ndossche)
. Fix OSS-Fuzz #568005340 (FETCH_DIM_FUNC_ARG partial conversion). (ndossche)
. Fix exception checks due to NAN warnings and object empty()'s. (ndossche)
+ . Fixed bug GH-24270 (JIT loses the called scope of self:: and parent:: calls
+ in closures). (RV7PR)
- PDO:
. Fixed a duplicate warning when ATTR_STATEMENT_CLASS is rejected on a
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 471cf55c654..08704742f8b 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -9344,7 +9344,7 @@ static int zend_jit_init_static_method_call(zend_jit_ctx *jit,
ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce))));
ir_IF_FALSE(if_object);
ir_END_PHI_list(values, ir_LOAD_A(jit_EX(This.value.ref)));
- ir_PHI_list(values);
+ scope_ref = ir_PHI_list(values);
} else {
scope_ref = ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce)));
}
diff --git a/ext/opcache/tests/jit/gh24270.inc b/ext/opcache/tests/jit/gh24270.inc
new file mode 100644
index 00000000000..badcee80a4b
--- /dev/null
+++ b/ext/opcache/tests/jit/gh24270.inc
@@ -0,0 +1,74 @@
+<?php
+class Base {
+ public static function staticSelf(): Closure {
+ return fn() => self::make();
+ }
+ public function boundSelf(): Closure {
+ return fn() => self::make();
+ }
+ private static function make() {
+ return new static();
+ }
+}
+class Mid extends Base {
+ protected static function name() {
+ return static::class;
+ }
+}
+class Child extends Mid {
+ public function boundParent(): Closure {
+ return fn() => parent::name();
+ }
+}
+class GrandChild extends Child {}
+
+class ParentA {
+ public static function name() {
+ return static::class;
+ }
+}
+class ParentB {
+ public static function name() {
+ return static::class;
+ }
+}
+trait T {
+ public static function traitSelf(): Closure {
+ return fn() => self::make();
+ }
+ public function traitParent(): Closure {
+ return fn() => parent::name();
+ }
+ public static function make() {
+ return new static();
+ }
+}
+class A extends ParentA {
+ use T;
+}
+class ChildA extends A {}
+class B extends ParentB {
+ use T;
+}
+class ChildB extends B {}
+
+$closures = [
+ 'self from static method' => Child::staticSelf(),
+ 'bound self' => (new Child)->boundSelf(),
+ 'bound parent' => (new GrandChild)->boundParent(),
+ 'trait self A' => ChildA::traitSelf(),
+ 'trait self B' => ChildB::traitSelf(),
+ 'trait parent A' => (new ChildA)->traitParent(),
+ 'trait parent B' => (new ChildB)->traitParent(),
+];
+
+$results = [];
+for ($i = 0; $i < 100; $i++) {
+ foreach ($closures as $name => $closure) {
+ $result = $closure();
+ $results[$name][is_object($result) ? $result::class : $result] = true;
+ }
+}
+foreach ($results as $name => $classes) {
+ echo $name, ': ', implode(', ', array_keys($classes)), "\n";
+}
diff --git a/ext/opcache/tests/jit/gh24270_001.phpt b/ext/opcache/tests/jit/gh24270_001.phpt
new file mode 100644
index 00000000000..dcc1b8700d5
--- /dev/null
+++ b/ext/opcache/tests/jit/gh24270_001.phpt
@@ -0,0 +1,22 @@
+--TEST--
+GH-24270 001: Function JIT loses the called scope of self:: and parent:: calls in closures
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+opcache.jit_buffer_size=64M
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+require __DIR__ . '/gh24270.inc';
+?>
+--EXPECT--
+self from static method: Child
+bound self: Child
+bound parent: GrandChild
+trait self A: ChildA
+trait self B: ChildB
+trait parent A: ChildA
+trait parent B: ChildB
diff --git a/ext/opcache/tests/jit/gh24270_002.phpt b/ext/opcache/tests/jit/gh24270_002.phpt
new file mode 100644
index 00000000000..8425d412270
--- /dev/null
+++ b/ext/opcache/tests/jit/gh24270_002.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GH-24270 002: Tracing JIT loses the called scope of self:: and parent:: calls in closures
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=tracing
+opcache.jit_buffer_size=64M
+opcache.jit_hot_func=1
+opcache.jit_hot_loop=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+require __DIR__ . '/gh24270.inc';
+?>
+--EXPECT--
+self from static method: Child
+bound self: Child
+bound parent: GrandChild
+trait self A: ChildA
+trait self B: ChildB
+trait parent A: ChildA
+trait parent B: ChildB