Commit 1f50b63369a for php.net

commit 1f50b63369ab42bde33b2ad71be57736a689c459
Author: Levi Morrison <levi.morrison@datadoghq.com>
Date:   Thu Apr 30 15:53:47 2026 -0400

    Fix JIT vm_interrupt (#21910)

    * Add observer VM interrupt JIT regression test

    * Fix observer VM interrupt during tracing JIT calls

diff --git a/NEWS b/NEWS
index ef713ddd725..a6fe8e2136a 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.4.22

+- Opcache:
+  . Fixed tracing JIT crash when a VM interrupt is handled during an observed
+    user function call. (Levi Morrison)
+
 - Standard:
   . Fixed bug GH-21689 (version_compare() incorrectly handles versions ending
     with a dot). (timwolla)
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 4251d6b891c..1346d141754 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -10337,28 +10337,19 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen
 		if (ZEND_OBSERVER_ENABLED && (!func || (func->common.fn_flags & (ZEND_ACC_CALL_VIA_TRAMPOLINE | ZEND_ACC_GENERATOR)) == 0)) {
 			ir_ref observer_handler;
 			ir_ref rx = jit_FP(jit);
+			const zend_op *observer_opline = NULL;
 			struct jit_observer_fcall_is_unobserved_data unobserved_data = jit_observer_fcall_is_unobserved_start(jit, func, &observer_handler, rx, func_ref);
 			if (trace && (trace->op != ZEND_JIT_TRACE_END || trace->stop != ZEND_JIT_TRACE_STOP_INTERPRETER)) {
 				ZEND_ASSERT(trace[1].op == ZEND_JIT_TRACE_VM || trace[1].op == ZEND_JIT_TRACE_END);
-				jit_SET_EX_OPLINE(jit, trace[1].opline);
+				observer_opline = trace[1].opline;
+				jit_SET_EX_OPLINE(jit, observer_opline);
 			} else if (GCC_GLOBAL_REGS) {
 				// EX(opline) = opline
 				ir_STORE(jit_EX(opline), jit_IP(jit));
 			}
 			jit_observer_fcall_begin(jit, rx, observer_handler);

-			if (trace) {
-				int32_t 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;
-				}
-			} else {
-				exit_addr = NULL;
-			}
-
-			zend_jit_check_timeout(jit, NULL /* we're inside the called function */, exit_addr);
+			zend_jit_check_timeout(jit, observer_opline, NULL);

 			jit_observer_fcall_is_unobserved_end(jit, &unobserved_data);
 		}
diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c
index 31052ec830f..0dfb62723bc 100644
--- a/ext/zend_test/observer.c
+++ b/ext/zend_test/observer.c
@@ -78,6 +78,10 @@ static void observer_begin(zend_execute_data *execute_data)
 {
 	assert_observer_opline(execute_data);

+	if (ZT_G(observer_set_vm_interrupt_on_begin)) {
+		zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
+	}
+
 	if (!ZT_G(observer_show_output)) {
 		return;
 	}
@@ -146,6 +150,14 @@ static void observer_end(zend_execute_data *execute_data, zval *retval)
 	}
 }

+static void (*zend_test_prev_interrupt_function)(zend_execute_data *execute_data);
+static void zend_test_interrupt_function(zend_execute_data *execute_data)
+{
+	if (zend_test_prev_interrupt_function) {
+		zend_test_prev_interrupt_function(execute_data);
+	}
+}
+
 static void observer_show_init(zend_function *fbc)
 {
 	if (fbc->common.function_name) {
@@ -361,6 +373,7 @@ PHP_INI_BEGIN()
 	STD_PHP_INI_BOOLEAN("zend_test.observer.show_init_backtrace", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_show_init_backtrace, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.observer.show_opcode", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_show_opcode, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_ENTRY("zend_test.observer.show_opcode_in_user_handler", "", PHP_INI_SYSTEM, OnUpdateString, observer_show_opcode_in_user_handler, zend_zend_test_globals, zend_test_globals)
+	STD_PHP_INI_BOOLEAN("zend_test.observer.set_vm_interrupt_on_begin", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_set_vm_interrupt_on_begin, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_init", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_init, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_switch", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_switch, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.observer.fiber_destroy", "0", PHP_INI_SYSTEM, OnUpdateBool, observer_fiber_destroy, zend_zend_test_globals, zend_test_globals)
@@ -398,10 +411,20 @@ void zend_test_observer_init(INIT_FUNC_ARGS)
 		zend_test_prev_execute_internal = zend_execute_internal;
 		zend_execute_internal = zend_test_execute_internal;
 	}
+
+	if (ZT_G(observer_set_vm_interrupt_on_begin)) {
+		zend_test_prev_interrupt_function = zend_interrupt_function;
+		zend_interrupt_function = zend_test_interrupt_function;
+	}
 }

 void zend_test_observer_shutdown(SHUTDOWN_FUNC_ARGS)
 {
+	if (zend_interrupt_function == zend_test_interrupt_function) {
+		zend_interrupt_function = zend_test_prev_interrupt_function;
+		zend_test_prev_interrupt_function = NULL;
+	}
+
 	if (type != MODULE_TEMPORARY) {
 		UNREGISTER_INI_ENTRIES();
 	}
diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h
index 7ec6f543123..c1310db7bd7 100644
--- a/ext/zend_test/php_test.h
+++ b/ext/zend_test/php_test.h
@@ -45,6 +45,7 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test)
 	int observer_show_init_backtrace;
 	int observer_show_opcode;
 	char *observer_show_opcode_in_user_handler;
+	int observer_set_vm_interrupt_on_begin;
 	int observer_nesting_depth;
 	int observer_fiber_init;
 	int observer_fiber_switch;
diff --git a/ext/zend_test/tests/observer_jit_vm_interrupt.inc b/ext/zend_test/tests/observer_jit_vm_interrupt.inc
new file mode 100644
index 00000000000..426d9fdc2cb
--- /dev/null
+++ b/ext/zend_test/tests/observer_jit_vm_interrupt.inc
@@ -0,0 +1,8 @@
+<?php
+
+namespace ZendTestJitInterrupt;
+
+function external_target(mixed $value): mixed
+{
+	return $value;
+}
diff --git a/ext/zend_test/tests/observer_jit_vm_interrupt.phpt b/ext/zend_test/tests/observer_jit_vm_interrupt.phpt
new file mode 100644
index 00000000000..4c9b21c9e9a
--- /dev/null
+++ b/ext/zend_test/tests/observer_jit_vm_interrupt.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Observer: VM interrupt during tracing JIT user function call
+--EXTENSIONS--
+opcache
+zend_test
+--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=1
+opcache.jit_hot_func=1
+opcache.jit_hot_return=1
+opcache.jit_hot_side_exit=1
+opcache.jit_max_polymorphic_calls=0
+zend_test.observer.enabled=1
+zend_test.observer.show_output=0
+zend_test.observer.observe_function_names=ZendTestJitInterrupt\external_target
+zend_test.observer.set_vm_interrupt_on_begin=1
+--SKIPIF--
+<?php
+if (ini_get('opcache.jit') === false) die('skip JIT not available');
+?>
+--FILE--
+<?php
+namespace ZendTestJitInterrupt;
+
+// Keep the callee in a separate file so the caller uses
+// INIT_NS_FCALL_BY_NAME/DO_FCALL_BY_NAME, not INIT_FCALL/DO_UCALL.
+require __DIR__ . '/observer_jit_vm_interrupt.inc';
+
+function drive_probe(int $n): int
+{
+    $sum = 0;
+    for ($i = 0; $i < $n; $i++) {
+        $sum += external_target($i);
+    }
+    return $sum;
+}
+
+$total = 0;
+for ($round = 0; $round < 300; $round++) {
+    $total += drive_probe(128);
+}
+
+echo "total={$total}\n";
+?>
+--EXPECT--
+total=2438400