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