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