From cb3579dc7a057b5fcfbd685a941fc2d7f5324a0d Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:45:40 +0200 Subject: [PATCH 1/2] Fix zend_jit_trace_find_init_fcall_op() (#23449) zend_jit_trace_find_init_fcall_op() tries to find the INIT_FCALL opline corresponding to a ZEND_JIT_TRACE_INIT_CALL record, but it fails to do so in the ZEND_JIT_TRACE_FAKE_INIT_CALL case, for nested calls. The first loop is supposed to find the first opline after the sequence of ZEND_JIT_TRACE_INIT_CALL record, but it mistakenly decrements 'p' after initially incrementing it. As a result 'p' eventually points to an invalid record. It works for non-nested calls because the 'p->op == ZEND_JIT_TRACE_VM' condition is true on the first iteration in that case. This can not lead to a crash or miscompilations, but this results in lost optimization opportunities. --- ext/opcache/jit/zend_jit_trace.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 49b8e29c1871..5408b29fc4e8 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -1167,6 +1167,9 @@ static const zend_op *zend_jit_trace_find_init_fcall_op(zend_jit_trace_rec *p, c const zend_op *opline = NULL; int call_level = 0; + /* Scan trace buffer forward to find the first recorded opline after + * the sequence of ZEND_JIT_TRACE_INIT_CALL, and keep track of the + * call level. */ p++; while (1) { if (p->op == ZEND_JIT_TRACE_VM) { @@ -1178,8 +1181,9 @@ static const zend_op *zend_jit_trace_find_init_fcall_op(zend_jit_trace_rec *p, c } else { return NULL; } - p--; + p++; } + /* Scan oplines backward to find the init fcall op */ if (opline) { while (opline > op_array->opcodes) { opline--; From 1053403d9aa48e64616ca6284d2cba7deb34671e Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:00:56 +0200 Subject: [PATCH 2/2] JIT: Record fake init calls for subtraces (fixes array_map optimization) (#23450) The array_map optimization may emit loops inside an INIT_FCALL-DO_FCALL sequence, which was not possible before. JIT doesn't expect that and forgets about pending calls when starting a subtrace for the loop. Fix by recording fake init calls in zend_jit_trace_subtrace(). --- ext/opcache/jit/zend_jit_vm_helpers.c | 18 ++++++++- .../jit/array_map_loop_in_call_region.phpt | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 ext/opcache/tests/jit/array_map_loop_in_call_region.phpt diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c index b01c3aaac62e..688d523b254c 100644 --- a/ext/opcache/jit/zend_jit_vm_helpers.c +++ b/ext/opcache/jit/zend_jit_vm_helpers.c @@ -680,11 +680,20 @@ static int zend_jit_trace_record_fake_init_call(zend_execute_data *call, zend_ji return zend_jit_trace_record_fake_init_call_ex(call, trace_buffer, idx, is_megamorphic, 0); } -static int zend_jit_trace_subtrace(zend_jit_trace_rec *trace_buffer, int start, int end, uint8_t event, const zend_op_array *op_array, const zend_op *opline) +static int zend_jit_trace_subtrace(zend_execute_data *call, zend_jit_trace_rec *trace_buffer, int start, int end, uint8_t event, const zend_op_array *op_array, const zend_op *opline) { int idx; TRACE_START(ZEND_JIT_TRACE_START, event, op_array, opline); + if (call) { + idx = zend_jit_trace_record_fake_init_call(call, trace_buffer, idx, 0); + if (idx < 0) { + return idx; + } + } + if (idx + (end - start) >= JIT_G(max_trace_length) - 2) { + return -1; + } memmove(trace_buffer + idx, trace_buffer + start, (end - start) * sizeof(zend_jit_trace_rec)); return idx + (end - start); } @@ -1352,8 +1361,13 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex, if (opline == last_loop_opline && level == last_loop_level) { - idx = zend_jit_trace_subtrace(trace_buffer, + int ret = zend_jit_trace_subtrace(EX(call), trace_buffer, last_loop, idx, ZEND_JIT_TRACE_START_LOOP, op_array, opline); + if (ret < 0) { + stop = ZEND_JIT_TRACE_STOP_TOO_LONG; + break; + } + idx = ret; start = ZEND_JIT_TRACE_START_LOOP; stop = ZEND_JIT_TRACE_STOP_LOOP; ret_level = 0; diff --git a/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt b/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt new file mode 100644 index 000000000000..acdb969e972d --- /dev/null +++ b/ext/opcache/tests/jit/array_map_loop_in_call_region.phpt @@ -0,0 +1,40 @@ +--TEST-- +JIT: array_map() foreach optimization: loop trace inside a call region must not clobber EX(call) +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECT-- +int(18)