Commit 1053403d9aa for php.net

commit 1053403d9aa48e64616ca6284d2cba7deb34671e
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Fri Sep 4 18:00:56 2026 +0200

    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().

diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c
index b01c3aaac62..688d523b254 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 00000000000..acdb969e972
--- /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--
+<?php
+
+class C {
+    static function f($x) { return $x + 1; }
+}
+
+function sink(array $a) { return array_sum($a); }
+
+/* array_map() is compiled to a foreach loop, so both loop headers sit between
+ * the INIT_FCALL of sink() and its DO_UCALL. The side trace recorded at the
+ * exit of the first loop runs into the header of the second one and is turned
+ * into a loop trace rooted there; that conversion must not lose track of the
+ * sink() frame that is still under construction, otherwise DO_FCALL of the
+ * callback stores NULL into EX(call) and the following SEND_VAL crashes.
+ *
+ * $o::f(...) is used so that INIT_STATIC_METHOD_CALL falls back to the VM
+ * handler, which is what makes the JIT emit the EX(call) store at DO_FCALL. */
+function test(array $a, $o) {
+    $n  = sink(array_map($o::f(...), $a));
+    $n += sink(array_map($o::f(...), $a));
+    return $n;
+}
+
+$a = [1, 2, 3];
+$o = new C();
+
+for ($i = 0; $i < 2; $i++) {
+    $r = test($a, $o);
+}
+
+var_dump($r);
+
+?>
+--EXPECT--
+int(18)