[php-src] master: Fix GH-21006: JIT SEGV with property hooks and FETCH_OBJ_FUNC_ARG (#21369)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-06-29T19:59:17-04:00

Commit: https://github.com/php/php-src/commit/ae00731fd2acba692d179fd1262ed19b9afa37bf
Raw diff: https://github.com/php/php-src/commit/ae00731fd2acba692d179fd1262ed19b9afa37bf.diff

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

Changed paths:
  A  ext/opcache/tests/jit/gh21006.phpt
  M  ext/opcache/jit/zend_jit_ir.c
  M  ext/opcache/jit/zend_jit_trace.c
  M  ext/opcache/jit/zend_jit_vm_helpers.c


Diff:

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 2fe0b1896a92..d62ef95b5513 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 024a5d0e194d..77bfaa03473a 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 271d923598d9..85a81c1573bc 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 000000000000..a201391505ab
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.