[php-src] Issue #24270: JIT loses called scope for forwarding self:: call from closure, causing new static() to resolve to Base

From: Date: Mon, 12 Oct 2026 07:17:19 +0000
Subject: [php-src] Issue #24270: JIT loses called scope for forwarding self:: call from closure, causing new static() to resolve to Base
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252977@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24270 Author: pdombrovsky ### Description Child::callback() returns a closure calling a private Base factory via self::make(). self::make() resolves the method itself in Base scope, but it is a forwarding static call and must preserve Child as the called class. Therefore new static() inside Base::make() must instantiate Child. The closure's Base scope permits access to the private method. PHP documentation: https://www.php.net/manual/en/language.oop5.late-static-bindings.php https://www.php.net/manual/en/language.oop5.visibility.php ## Reproducer Save as repro.php; no dependencies. ```php <?php abstract class Base { public static function callback(): Closure { return fn() => self::make(); } private static function make() { return new static(); } } class Child extends Base {} $callback = Child::callback(); for ($i = 0; $i < 10000; ++$i) { $object = $callback(); if ($object::class !== Child::class) { throw new RuntimeException('Wrong class: ' . $object::class); } } echo "OK\n"; ``` ## Run ```sh php -n \ -d opcache.enable_cli=1 \ -d opcache.file_update_protection=0 \ -d opcache.jit_buffer_size=64M \ -d opcache.jit=tracing \ repro.php ``` Repeat with opcache.jit=function and opcache.jit=off. If OPcache is a shared extension (e.g. the PHP 8.4 Docker image), also add: ```sh -d zend_extension=opcache ``` The tested PHP 8.5 builds have OPcache built in. ## Expected result OK in all three modes. ## Actual result with tracing/function JIT ```text Fatal error: Uncaught Error: Cannot instantiate abstract class Base in /repro/repro.php:12 Stack trace: #0 /repro/repro.php(7): Base::make() #1 /repro/repro.php(20): Base::{closure:Base::callback():7}() #2 {main} thrown in /repro/repro.php on line 12 ``` ## Version comparison Fresh CLI processes; up to 10,000 calls; default JIT counters. | PHP | JIT off | tracing JIT | function JIT | |---|---|---|---| | 8.4.26 | OK | OK | OK | | 8.5.0 | OK | error | error | | 8.5.11 | OK | error | error | Last unaffected tested stable version: 8.4.26. First affected stable release of PHP 8.5: 8.5.0; still affected in 8.5.11. Pre-release builds were not checked. JIT activation, script caching and generated JIT code were verified; passing runs were not caused by an inactive JIT. On 8.5.0/8.5.11, an instrumented run failed at callback call 1 with function JIT and call 62 with tracing JIT (jit_hot_loop=61). Function JIT compiles functions on script load; tracing JIT compiles frequently executed paths later. The call number depends on JIT counters and execution flow: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit ## Controls Edit the same reproducer and re-run the commands above. - Change only self::make() to static::make(): OK in all three modes on 8.5.11. - Remove only abstract from Base: JIT off gives OK; tracing/function JIT create Base instead of Child and throw Wrong class: Base on 8.5.11. [repro.php](https://github.com/user-attachments/files/33325236/repro.php) ### PHP Version ```plain PHP 8.5.11 (cli) (built: Sep 24 2026 19:10:16) (NTS) Copyright (c) The PHP Group Built by https://github.com/docker-library/php Zend Engine v4.5.11, Copyright (c) Zend Technologies with Zend OPcache v8.5.11, Copyright (c), by Zend Technologies ``` ### Operating System Alpine Linux 3.24.2, x86_64, Docker.

« previous php.bugs (#252977) next »