Skip to content

Fix GH-21006: JIT SEGV with FETCH_OBJ_FUNC_ARG and property hooks#21369

Open
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21006-jit-fetch-obj-func-arg-segv
Open

Fix GH-21006: JIT SEGV with FETCH_OBJ_FUNC_ARG and property hooks#21369
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21006-jit-fetch-obj-func-arg-segv

Conversation

@iliaal
Copy link
Contributor

@iliaal iliaal commented Mar 7, 2026

Summary

  • When JIT falls back to the VM handler for FETCH_OBJ_FUNC_ARG (or FETCH_OBJ_R), the handler may find the SIMPLE_GET flag set in the runtime cache for a hooked property. This causes it to push a call frame for the hook function, changing execute_data. When the trace resumes, it continues with the wrong execute_data, leading to a segfault.
  • Fix: emit IR code in zend_jit_trace_handler() to clear the SIMPLE_GET flag before calling the VM handler, so it falls through to read_property instead.

Closes GH-21006

When the JIT falls back to the VM handler for FETCH_OBJ_FUNC_ARG (or
FETCH_OBJ_R), the handler may find the SIMPLE_GET flag set in the
runtime cache for a hooked property. This causes it to push a call
frame for the hook function, changing execute_data. When the trace
resumes after the handler returns, it continues with the wrong
execute_data, leading to a segfault on the next opcode that accesses
EX(call).

Fix by emitting IR code in zend_jit_trace_handler() to clear the
SIMPLE_GET flag in the runtime cache before calling the VM handler,
so it falls through to read_property instead. The flag is only cleared
when the cached property offset is in the hooked property range (1..15),
to avoid corrupting other offset types like ZEND_DYNAMIC_PROPERTY_OFFSET.
@iliaal iliaal force-pushed the fix/gh-21006-jit-fetch-obj-func-arg-segv branch from 5963656 to a0251cf Compare March 7, 2026 17:04
@dstogov
Copy link
Member

dstogov commented Mar 16, 2026

I can reproduce the problem with "opcache.jit_hot_func=1 opcache.jit_hot_loop=1 opcache.jit_hot_return=1 opcache.jit_hot_side_exit=1". The second loop in the test seems irrelevant.

It seems like this was partially fixed via 57c62eb (missing support for FETCH_OBJ_FUNC_ARG), but than was reverted...

@ndossche @iluuu1994 can you take a look.

@iliaal
Copy link
Contributor Author

iliaal commented Mar 16, 2026

I can reproduce the problem with "opcache.jit_hot_func=1 opcache.jit_hot_loop=1 opcache.jit_hot_return=1 opcache.jit_hot_side_exit=1". The second loop in the test seems irrelevant.

The second loop guards against a regression in the fix itself: the SIMPLE_GET flag clear uses a range check (offset 1..15) to avoid corrupting ZEND_DYNAMIC_PROPERTY_OFFSET for non-hooked properties. Class D exercises that path. You are right, but it does safety check for current fix.

@iliaal
Copy link
Contributor Author

iliaal commented Mar 16, 2026

Regarding the fix itself — clearing SIMPLE_GET before the handler call is a workaround that forces the slower read_property path. A better approach would be a post-handler frame guard in zend_jit_trace_handler(): compare EG(current_execute_data) with the pre-call FP after the handler returns, and exit to the VM if SIMPLE_GET entered a hook. Hot path cost is one load + compare + cold branch (vs ~10 ops to check/clear the cache), and SIMPLE_GET's inline frame setup is preserved.

diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -16850,12 +16850,15 @@ static int zend_jit_trace_handler(...)
 	ir_ref ref;
 
 	zend_jit_set_ip(jit, opline);
+	/* Save FP before handler call to detect SIMPLE_GET hook frame push */
+	ir_ref saved_fp = (opline->opcode == ZEND_FETCH_OBJ_R
+	 || opline->opcode == ZEND_FETCH_OBJ_FUNC_ARG) ? jit_FP(jit) : IR_UNUSED;
+
 	if (GCC_GLOBAL_REGS) {
 		ir_CALL(IR_VOID, ir_CONST_FUNC(handler));
 	} else {
 		ref = jit_FP(jit);
 		ref = ir_CALL_1(IR_I32, ir_CONST_FC_FUNC(handler), ref);
 	}
+
+	if (saved_fp != IR_UNUSED) {
+		/* FETCH_OBJ_R handler may enter a property hook via SIMPLE_GET,
+		 * pushing a call frame and changing EG(current_execute_data).
+		 * Detect this and exit to the VM to execute the hook. */
+		ir_ref if_hook = ir_IF(ir_NE(ir_LOAD_A(jit_EG(current_execute_data)), saved_fp));
+		ir_IF_TRUE_cold(if_hook);
+		if (GCC_GLOBAL_REGS) {
+			ir_TAILCALL(IR_VOID, ir_LOAD_A(jit_IP(jit)));
+		} else {
+			jit_STORE_FP(jit, ir_LOAD_A(jit_EG(current_execute_data)));
+			ir_RETURN(ir_CONST_I32(1)); /* ZEND_VM_ENTER */
+		}
+		ir_IF_FALSE(if_hook);
+	}
+
 	if (may_throw

Happy to implement this, or defer to @ndossche / @iluuu1994 if the proper fix should be at a different layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT has wrong expectations for ZEND_FETCH_OBJ_FUNC_ARG when the prop is hooked

2 participants