[php-src] Issue #24270: JIT loses called scope for forwarding self:: call from closure, causing new static() to resolve to Base
| From: | pdombrovsky | 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.