Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ PHP NEWS
. 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)
. Fixed bug GH-24286 (Tracing JIT loses dynamic call results after
Closure::__invoke). (ndossche, Alessio Attilio)
. Fixed bug GH-24337 (JIT: segfault when decrementing a non-numeric string
property). (Lazizbek Ergashev)
. Fixed bug GH-24185 (Windows ZTS: random access violations with OPcache JIT
Expand Down
34 changes: 31 additions & 3 deletions ext/opcache/jit/zend_jit_ir.c
Original file line number Diff line number Diff line change
Expand Up @@ -10068,6 +10068,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
const void *exit_addr = NULL;
const zend_op *prev_opline;
ir_ref rx, func_ref = IR_UNUSED, if_user = IR_UNUSED, user_path = IR_UNUSED;
bool unknown_internal = 0;

prev_opline = opline - 1;
while (prev_opline->opcode == ZEND_EXT_FCALL_BEGIN || prev_opline->opcode == ZEND_TICKS) {
Expand Down Expand Up @@ -10151,14 +10152,39 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
if (opline->opcode == ZEND_DO_FCALL || opline->opcode == ZEND_DO_FCALL_BY_NAME) {
if (!func) {
if (trace) {
uint32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM);
uint32_t exit_point;

func_ref = ir_LOAD_A(jit_CALL(rx, func));

if (trace->op == ZEND_JIT_TRACE_DO_ICALL) {
/* The trace recorded a call to an unknown internal function (e.g. a trampoline)
* and continues in the caller, so the callee must be internal.
*
* If a user function is called instead, exit and let a side trace handle it.
* This is better than emitting a user function code path:
* - the trace code after the call has a single known state,
* - the trace doesn't contain code for the user call that is rarely used,
* - a side trace knows the called function and can inline it. */
exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_POLYMORPHISM);
exit_addr = zend_jit_trace_get_exit_addr(exit_point);
if (!exit_addr) {
return 0;
}

ir_GUARD(
ir_EQ(
ir_LOAD_U8(ir_ADD_OFFSET(func_ref, offsetof(zend_function, type))),
ir_CONST_U8(ZEND_INTERNAL_FUNCTION)),
ir_CONST_ADDR(exit_addr));
unknown_internal = 1;
}

exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM);
exit_addr = zend_jit_trace_get_exit_addr(exit_point);
if (!exit_addr) {
return 0;
}

func_ref = ir_LOAD_A(jit_CALL(rx, func));
ir_GUARD_NOT(
ir_AND_U32(
ir_LOAD_U32(ir_ADD_OFFSET(func_ref, offsetof(zend_op_array, fn_flags))),
Expand Down Expand Up @@ -10232,6 +10258,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
}

if (!func
&& !unknown_internal
&& opline->opcode != ZEND_DO_UCALL
&& opline->opcode != ZEND_DO_ICALL) {
ir_ref type_ref = ir_LOAD_U8(ir_ADD_OFFSET(func_ref, offsetof(zend_function, type)));
Expand All @@ -10240,6 +10267,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
}

if ((!func || func->type == ZEND_USER_FUNCTION)
&& !unknown_internal
&& opline->opcode != ZEND_DO_ICALL) {
bool recursive_call_through_jmp = 0;
uint32_t num_args = 0;
Expand Down Expand Up @@ -10546,7 +10574,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen

if ((!func || func->type == ZEND_INTERNAL_FUNCTION)
&& (opline->opcode != ZEND_DO_UCALL)) {
if (!func && (opline->opcode != ZEND_DO_ICALL)) {
if (!func && !unknown_internal && (opline->opcode != ZEND_DO_ICALL)) {
ir_IF_FALSE(if_user);
}

Expand Down
34 changes: 34 additions & 0 deletions ext/opcache/tests/jit/gh24286.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
GH-24286 (Tracing JIT preserves dynamic call results after Closure::__invoke)
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit_buffer_size=64M
opcache.jit=tracing
opcache.jit_hot_loop=61
opcache.jit_hot_func=127
opcache.jit_hot_return=8
opcache.jit_hot_side_exit=8
--FILE--
<?php
class A {
function m() { return false; }
}
function call($object, $method) {
return $object->{$method}();
}
function every($items, $method) {
foreach ($items as $item) {
if (!call($item, $method)) return false;
}
return true;
}
$closure = function () { return false; };
for ($i = 0; $i < 2000; $i++) every([$closure], '__invoke');
var_dump(call(new A, 'm'));
?>
--EXPECT--
bool(false)
47 changes: 47 additions & 0 deletions ext/opcache/tests/jit/gh24286_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
GH-24286 (Tracing JIT preserves unpacked dynamic call results during recursion)
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit_buffer_size=64M
opcache.jit=tracing
opcache.jit_hot_loop=61
opcache.jit_hot_func=127
opcache.jit_hot_return=8
opcache.jit_hot_side_exit=8
--FILE--
<?php
class Box {
function run(float $value): mixed { return $value; }
}
function invokeCall($object, $method, ...$args) {
return $object->{$method}(...$args);
}
function numberText(mixed $value): string {
if (!is_int($value) && !is_float($value)) return '';
return (string) $value;
}
function walk($items, $method, $value, int $depth): string {
$text = '';
foreach ($items as $item) {
$result = invokeCall($item, $method, $value);
$text .= numberText($result);
if ($result === false) return '';
if ($depth > 0) $text .= walk([$item], $method, $value + 0.5, $depth - 1);
}
return $text;
}
$closure = function (float $value): mixed { return false; };
for ($i = 0; $i < 2000; $i++) walk([$closure], '__invoke', 0.5, 1);
for ($i = 0; $i < 500; $i++) {
walk([new Box()], 'run', -184013402102942.84 + $i, 2);
}
echo walk([new Box()], 'run', 15.0, 2), "\n";
var_dump(invokeCall(new Box(), 'run', value: 15.5));
?>
--EXPECT--
1515.516
float(15.5)
57 changes: 57 additions & 0 deletions ext/opcache/tests/jit/gh24286_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
GH-24286 (Tracing JIT preserves polymorphic call results and exceptions after Closure::__invoke)
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit_buffer_size=64M
opcache.jit=tracing
opcache.jit_hot_loop=61
opcache.jit_hot_func=127
opcache.jit_hot_return=8
opcache.jit_hot_side_exit=8
--FILE--
<?php
class Value {
public $value;
function get() { return $this->value; }
function fail() { throw new RuntimeException('expected'); }
function __call($method, $args) { return $this->value; }
}
function call($object, $method) {
return $object->{$method}();
}
function every($items, $method) {
foreach ($items as $item) {
if (!call($item, $method)) return false;
}
return true;
}
$closure = function () { return false; };
for ($i = 0; $i < 2000; $i++) every([$closure], '__invoke');
$object = new Value;
foreach ([null, false, true, 42, 1.5, str_repeat('x', 3), [42], (object) ['n' => 42]] as $value) {
$object->value = $value;
for ($i = 0; $i < 100; $i++) {
if (call($object, 'get') !== $value) {
throw new RuntimeException('Incorrect result');
}
}
}
$object->value = 'magic';
var_dump(call($object, 'missing'));
var_dump(call($closure, '__invoke'));
try {
call($object, 'fail');
} catch (RuntimeException $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
echo "Done\n";
?>
--EXPECT--
string(5) "magic"
bool(false)
RuntimeException: expected
Done
Loading