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
4 changes: 3 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.3.37


- Opcache:
. Fixed bug GH-25247 (Tracing JIT uses a stale value for argument 13 and
above). (Rafael Molina)

21 Oct 2026, PHP 8.3.36

Expand Down
35 changes: 35 additions & 0 deletions ext/opcache/jit/zend_jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,33 @@ static int sp_adj[SP_ADJ_LAST];
# pragma clang diagnostic pop
#endif

/* SEND_*_EX above MAX_ARG_FLAG_NUM is left to the VM handler, which reads
* the operand from its stack slot. The allocator may still keep a long or
* double in a register for that operand (ZREG_LAST_USE, without ZREG_STORE),
* so zend_jit_store_var_if_necessary() does not write it back. GH-25247.
*/
static int zend_jit_spill_op1_before_vm_send(dasm_State **Dst, const zend_op *opline,
zend_jit_addr op1_addr, uint32_t op1_info, zend_jit_trace_stack *stack)
{
uint32_t concrete = op1_info & MAY_BE_ANY;

if (!(opline->op1_type & (IS_TMP_VAR|IS_VAR))
|| (concrete != MAY_BE_LONG && concrete != MAY_BE_DOUBLE)
|| Z_MODE(op1_addr) != IS_REG
|| Z_LOAD(op1_addr)) {
return 1;
}
if (!zend_jit_spill_store(Dst, op1_addr,
ZEND_ADDR_MEM_ZVAL(ZREG_FP, opline->op1.var), op1_info, 1)) {
return 0;
}
if (stack) {
SET_STACK_TYPE(stack, EX_VAR_TO_NUM(opline->op1.var),
concrete == MAY_BE_LONG ? IS_LONG : IS_DOUBLE, 1);
}
return 1;
}

#if _WIN32
# include <Windows.h>
#else
Expand Down Expand Up @@ -3388,6 +3415,10 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
}
if (opline->opcode == ZEND_SEND_VAL_EX
&& opline->op2.num > MAX_ARG_FLAG_NUM) {
if (!zend_jit_spill_op1_before_vm_send(&dasm_state, opline,
OP1_REG_ADDR(), OP1_INFO(), NULL)) {
goto jit_failure;
}
break;
}
if (!zend_jit_send_val(&dasm_state, opline,
Expand Down Expand Up @@ -3417,6 +3448,10 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
if ((opline->opcode == ZEND_SEND_VAR_EX
|| opline->opcode == ZEND_SEND_VAR_NO_REF_EX)
&& opline->op2.num > MAX_ARG_FLAG_NUM) {
if (!zend_jit_spill_op1_before_vm_send(&dasm_state, opline,
OP1_REG_ADDR(), OP1_INFO(), NULL)) {
goto jit_failure;
}
break;
}
op1_addr = OP1_REG_ADDR();
Expand Down
5 changes: 4 additions & 1 deletion ext/opcache/jit/zend_jit_arm64.dasc
Original file line number Diff line number Diff line change
Expand Up @@ -15044,8 +15044,11 @@ static bool zend_jit_opline_supports_reg(const zend_op_array *op_array, zend_ssa
switch (opline->opcode) {
case ZEND_SEND_VAR:
case ZEND_SEND_VAL:
case ZEND_SEND_VAL_EX:
return (opline->op2_type != IS_CONST);
case ZEND_SEND_VAL_EX:
/* Argument 13+ falls back to the VM handler, which reads the slot. */
return (opline->op2_type != IS_CONST)
&& opline->op2.num <= MAX_ARG_FLAG_NUM;
case ZEND_QM_ASSIGN:
case ZEND_IS_SMALLER:
case ZEND_IS_SMALLER_OR_EQUAL:
Expand Down
8 changes: 8 additions & 0 deletions ext/opcache/jit/zend_jit_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -5134,6 +5134,10 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par
}
if (opline->opcode == ZEND_SEND_VAL_EX
&& opline->op2.num > MAX_ARG_FLAG_NUM) {
if (!zend_jit_spill_op1_before_vm_send(&dasm_state, opline,
OP1_REG_ADDR(), OP1_INFO(), stack)) {
goto jit_failure;
}
break;
}
op1_info = OP1_INFO();
Expand Down Expand Up @@ -5180,6 +5184,10 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par
if ((opline->opcode == ZEND_SEND_VAR_EX
|| opline->opcode == ZEND_SEND_VAR_NO_REF_EX)
&& opline->op2.num > MAX_ARG_FLAG_NUM) {
if (!zend_jit_spill_op1_before_vm_send(&dasm_state, opline,
OP1_REG_ADDR(), OP1_INFO(), stack)) {
goto jit_failure;
}
break;
}
op1_addr = OP1_REG_ADDR();
Expand Down
5 changes: 4 additions & 1 deletion ext/opcache/jit/zend_jit_x86.dasc
Original file line number Diff line number Diff line change
Expand Up @@ -16036,8 +16036,11 @@ static bool zend_jit_opline_supports_reg(const zend_op_array *op_array, zend_ssa
switch (opline->opcode) {
case ZEND_SEND_VAR:
case ZEND_SEND_VAL:
case ZEND_SEND_VAL_EX:
return (opline->op2_type != IS_CONST);
case ZEND_SEND_VAL_EX:
/* Argument 13+ falls back to the VM handler, which reads the slot. */
return (opline->op2_type != IS_CONST)
&& opline->op2.num <= MAX_ARG_FLAG_NUM;
case ZEND_QM_ASSIGN:
case ZEND_IS_SMALLER:
case ZEND_IS_SMALLER_OR_EQUAL:
Expand Down
55 changes: 55 additions & 0 deletions ext/opcache/tests/jit/gh25247.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
GH-25247 (Tracing JIT: argument 13+ keeps a stale value when the operand is a QM_ASSIGN left in a register)
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit=tracing
opcache.jit_buffer_size=64M
opcache.jit_hot_loop=1
opcache.jit_hot_func=1
opcache.jit_hot_return=1
opcache.jit_hot_side_exit=1
--FILE--
<?php
function helper_int() { return 1; }
function helper_str() { return 's'; }

$f = function ($a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $a10, $a11, $a12, $a13) {
return $a13;
};
$f12 = function ($a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $a10, $a11, $a12, $a13) {
return $a12;
};

$o = new stdClass;
$o->x = 'x';
$o->y = 1;
$c = true;
$x = null;
$a = 1;
$b = 2;

for ($i = 0; $i < 5; $i++) {
$checks = [
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, $o->x, $o->y === 1 ? 1 : 2), 1],
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, $c ? helper_int() : 0), 1],
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, $c ? max($a, $b) : 0), 2],
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, $x ?? helper_int()), 1],
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, $o->y === 1 ? 1.5 : 2.5), 1.5],
[$f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, $c ? helper_str() : 'no'), 's'],
[$f12(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, $o->y === 1 ? 1 : 2, 13), 1],
];
foreach ($checks as $n => [$got, $expect]) {
if ($got !== $expect) {
echo "iteration $i check $n: ", var_export($got, true), ", expected ", var_export($expect, true), "\n";
exit(1);
}
}
}
echo "OK\n";
?>
--EXPECT--
OK
Loading