diff --git a/NEWS b/NEWS index 91bd9fa4a64e..f7d3ca924867 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index b58b515a6231..8bc42f51b813 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -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 #else @@ -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, @@ -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(); diff --git a/ext/opcache/jit/zend_jit_arm64.dasc b/ext/opcache/jit/zend_jit_arm64.dasc index a325bafdc4e6..6318e1a434cf 100644 --- a/ext/opcache/jit/zend_jit_arm64.dasc +++ b/ext/opcache/jit/zend_jit_arm64.dasc @@ -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: diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 33c554a3f78e..1e5a95134c59 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -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(); @@ -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(); diff --git a/ext/opcache/jit/zend_jit_x86.dasc b/ext/opcache/jit/zend_jit_x86.dasc index 7cf15c8e15da..48e08b0bdd7a 100644 --- a/ext/opcache/jit/zend_jit_x86.dasc +++ b/ext/opcache/jit/zend_jit_x86.dasc @@ -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: diff --git a/ext/opcache/tests/jit/gh25247.phpt b/ext/opcache/tests/jit/gh25247.phpt new file mode 100644 index 000000000000..0eb7ebbfe785 --- /dev/null +++ b/ext/opcache/tests/jit/gh25247.phpt @@ -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-- +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