Repository navigation
Tracing JIT loses the result of a dynamic method call traced with Closure::__invoke (8.5 regression) #24286
Description
Description
With the tracing JIT, the result of a dynamic method call
$o->{$m}()is lost when the call site was first traced while callingClosure::__invoke. The caller then reads whatever is left in the result slot.
The following code:
<?php
class A { function m() { return false; } }
function call($o, $m) { return $o->{$m}(); }
function every($items, $m) {
foreach ($items as $i) { if (!call($i, $m)) return false; }
return true;
}
$c = function () { return false; };
for ($i = 0; $i < 2000; $i++) every([$c], '__invoke');
var_dump(call(new A, 'm'));php -n -d opcache.enable_cli=1 -d opcache.jit_buffer_size=64M -d opcache.jit=tracing test.phpResulted in this output:
UNKNOWN:0
But I expected this output instead:
bool(false)
- Wrong on 8.5.0, 8.5.11 and PHP-8.5 @ bbfdd69, with
opcache.jit=tracingand with1054.- Correct with
opcache.jit=function, with the JIT off, and on 8.4.26 and 8.3.35 (tracing).- Not fixed by Fix GH-23572: JIT calls wrong static method from trait clones #23611 (Tracing JIT calls the wrong method for self::/parent:: static calls in trait methods shared by several classes #23572).
- The warm-up through
Closure::__invokeis required: warming upcall()with a plain object's method does not reproduce it. The earlyreturn falseinevery()is also required.
Valgrind on PHP-8.5 @ bbfdd69 (
-g -O2,USE_ZEND_ALLOC=0 valgrind --smc-check=all --track-origins=yes). Leaving out their_sparse_set_inreports from the JIT compiler, every remaining error is about the same never-written VM stack memory: it is used in JIT code, printed byvar_dump(), then destroyed:
Conditional jump or move depends on uninitialised value(s)
at 0x11800ADF: ???
by 0x8C4E9F: zend_execute (zend_vm_execute.h:122008)
by 0x9270D5: zend_execute_script (zend.c:1985)
Uninitialised value was created by a heap allocation
by 0x88CAE8: zend_vm_stack_new_page (zend_execute.h:327)
by 0x88CAE8: zend_vm_stack_init (zend_execute.c:195)
Conditional jump or move depends on uninitialised value(s)
at 0x7668EE: php_var_dump (var.c:130)
by 0x7672E0: zif_var_dump (var.c:257)
by 0x8C0273: ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER (zend_vm_execute.h:1360)
Uninitialised value was created by a heap allocation
by 0x88CAE8: zend_vm_stack_new_page (zend_execute.h:327)
by 0x88CAE8: zend_vm_stack_init (zend_execute.c:195)
Conditional jump or move depends on uninitialised value(s)
at 0x8C02AE: zval_ptr_dtor_nogc (zend_variables.h:35)
by 0x8C02AE: zend_vm_stack_free_args (zend_execute.h:405)
by 0x8C02AE: ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER (zend_vm_execute.h:1379)
Uninitialised value was created by a heap allocation
by 0x88CAE8: zend_vm_stack_new_page (zend_execute.h:327)
by 0x88CAE8: zend_vm_stack_init (zend_execute.c:195)
Real-world impact: Laravel's
HigherOrderCollectionProxyclosure has this shape. Telescope calls it with->every->__invoke($entry)for its filters, and then with->partition->isException()when storing entries. On a freshlaravel/laravel13.35 +laravel/telescope5.25 install (officialphp:8.5.11-cliimage, default JIT settings apart fromopcache.jit=tracing), an artisan command that logs, uses the cache and runs queries in a loop crashes withzend_mm_heap corruptedor SIGSEGV withAPP_ENV=production. It crashed in 28 of 32 runs from 250 iterations up. It does not crash with function JIT, with the JIT off, or on 8.4.26.
PHP Version
PHP 8.5.11 (cli) (built: Oct 6 2026 01:22:31) (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
Debian 13 (official Docker image), x86_64
Activity
ndossche commentedon Oct 13, 2026
Commit 097edc8 stopped aborting traces at internal trampolines like Closure::__invoke, so these calls are now recorded as unknown internal calls without a callee-type guard.
ndossche commentedon Oct 13, 2026
Please try if this patch works for your real-world workload:
liquidice commentedon Oct 13, 2026
Thanks, the patch works.