Commit ae00731fd2a for php.net
commit ae00731fd2acba692d179fd1262ed19b9afa37bf
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Jun 29 19:59:17 2026 -0400
Fix GH-21006: JIT SEGV with property hooks and FETCH_OBJ_FUNC_ARG (#21369)
FETCH_OBJ_FUNC_ARG reading a SIMPLE_GET property hook pushed the getter
call frame mid-trace and corrupted the call being built, crashing at the
following SEND_FUNC_ARG. Compile it inline like FETCH_OBJ_R, clearing the
SIMPLE_GET flag so the read takes read_property. Arguments to a known
callee resolve their by-ref-ness at compile time through the preceding
CHECK_FUNC_ARG; named arguments and arguments to an unknown callee (e.g.
a __call trampoline) get a run-time guard that deoptimizes to the
interpreter when the argument is passed by reference.
Fixes GH-21006
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 2fe0b1896a9..d62ef95b551 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -14234,6 +14234,26 @@ static int zend_jit_class_guard(zend_jit_ctx *jit, const zend_op *opline, ir_ref
return 1;
}
+static int zend_jit_func_arg_by_ref_guard(zend_jit_ctx *jit, const zend_op *opline)
+{
+ int32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM);
+ const void *exit_addr = zend_jit_trace_get_exit_addr(exit_point);
+ ir_ref rx, call_info;
+
+ if (!exit_addr) {
+ return 0;
+ }
+ if (jit->reuse_ip) {
+ rx = jit_IP(jit);
+ } else {
+ rx = ir_LOAD_A(jit_EX(call));
+ }
+ call_info = ir_LOAD_U32(jit_CALL(rx, This.u1.type_info));
+ ir_GUARD_NOT(ir_AND_U32(call_info, ir_CONST_U32(ZEND_CALL_SEND_ARG_BY_REF)),
+ ir_CONST_ADDR(exit_addr));
+ return 1;
+}
+
static int zend_jit_fetch_obj(zend_jit_ctx *jit,
const zend_op *opline,
const zend_op_array *op_array,
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 024a5d0e194..77bfaa03473 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -6037,10 +6037,15 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer,
case ZEND_FETCH_OBJ_FUNC_ARG:
if (!JIT_G(current_frame)
|| !JIT_G(current_frame)->call
- || !JIT_G(current_frame)->call->func
- || !TRACE_FRAME_IS_LAST_SEND_BY_VAL(JIT_G(current_frame)->call)) {
+ || TRACE_FRAME_IS_LAST_SEND_BY_REF(JIT_G(current_frame)->call)) {
break;
}
+ if (!JIT_G(current_frame)->call->func
+ || !TRACE_FRAME_IS_LAST_SEND_BY_VAL(JIT_G(current_frame)->call)) {
+ if (!zend_jit_func_arg_by_ref_guard(&ctx, opline)) {
+ goto jit_failure;
+ }
+ }
ZEND_FALLTHROUGH;
case ZEND_FETCH_OBJ_R:
case ZEND_FETCH_OBJ_IS:
diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c
index 271d923598d..85a81c1573b 100644
--- a/ext/opcache/jit/zend_jit_vm_helpers.c
+++ b/ext/opcache/jit/zend_jit_vm_helpers.c
@@ -978,6 +978,7 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
}
}
break;
+ case ZEND_FETCH_OBJ_FUNC_ARG:
case ZEND_FETCH_OBJ_R: {
if (opline->op2_type == IS_CONST) {
/* Remove the SIMPLE_GET flag to avoid inlining hooks. */
@@ -992,7 +993,6 @@ zend_jit_trace_stop ZEND_FASTCALL zend_jit_trace_execute(zend_execute_data *ex,
case ZEND_FETCH_OBJ_W:
case ZEND_FETCH_OBJ_RW:
case ZEND_FETCH_OBJ_IS:
- case ZEND_FETCH_OBJ_FUNC_ARG:
case ZEND_FETCH_OBJ_UNSET:
case ZEND_ASSIGN_OBJ:
case ZEND_ASSIGN_OBJ_OP:
diff --git a/ext/opcache/tests/jit/gh21006.phpt b/ext/opcache/tests/jit/gh21006.phpt
new file mode 100644
index 00000000000..a201391505a
--- /dev/null
+++ b/ext/opcache/tests/jit/gh21006.phpt
@@ -0,0 +1,79 @@
+--TEST--
+GH-21006: JIT SEGV with FETCH_OBJ_FUNC_ARG and property hooks
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=tracing
+opcache.jit_hot_loop=1
+opcache.jit_hot_func=1
+opcache.jit_hot_return=1
+opcache.jit_hot_side_exit=1
+--FILE--
+<?php
+namespace Test;
+
+class C
+{
+ public $prop {
+ get => 'sha256';
+ }
+
+ public function sign()
+ {
+ return hash_hmac(
+ algo: $this->prop,
+ data: '',
+ key: '',
+ );
+ }
+}
+
+$obj = new C();
+for ($i = 0; $i < 100; $i++) {
+ $obj->sign();
+}
+
+#[\AllowDynamicProperties]
+class D
+{
+ public function test()
+ {
+ return hash_hmac(
+ algo: $this->algo,
+ data: '',
+ key: '',
+ );
+ }
+}
+
+$d = new D();
+$d->algo = 'sha256';
+for ($i = 0; $i < 100; $i++) {
+ $d->test();
+}
+
+class E
+{
+ public $prop {
+ get => [];
+ }
+
+ public function sign()
+ {
+ (new Trampoline)->f($this->prop);
+ }
+}
+
+class Trampoline
+{
+ public function __call($name, $args) {}
+}
+
+$e = new E();
+for ($i = 0; $i < 100; $i++) {
+ $e->sign();
+}
+echo "OK\n";
+?>
+--EXPECT--
+OK