Commit 905afb53654 for php.net

commit 905afb536542079157a50048a85871890f746f5d
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Fri Aug 7 16:15:21 2026 +0200

    JIT: Preserve parent regs in zend_jit_deoptimizer_start() (#22916)

    Fixes GH-22915

diff --git a/NEWS b/NEWS
index 474db936ef1..ba832ef0588 100644
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,10 @@ PHP                                                                        NEWS
 - OpenSSL:
   . Fix missing error check on invalid alpn protocols. (ndossche)

+- Opcache:
+  . Fixed bug GH-22916 (Preserve parent regs in zend_jit_deoptimizer_start()).
+    (Arnaud)
+
 - PCRE:
   . Fixed bug GH-21134 (Crash with \C + UTF-8). Using \C in UTF-8 patterns is
     now forbidden. (Arnaud)
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index b48058196c9..4c20c115b84 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -338,6 +338,11 @@ static int zend_jit_assign_to_variable(zend_jit_ctx   *jit,
                                        zend_jit_addr   ref_addr,
                                        bool       check_exception);

+static void zend_jit_preserve_parent_regs(zend_jit_ctx *jit,
+                                          zend_ssa *ssa,
+                                          zend_jit_trace_info *parent,
+                                          uint32_t exit_num);
+
 typedef struct _zend_jit_stub {
 	const char *name;
 	int (*stub)(zend_jit_ctx *jit);
@@ -17043,6 +17048,7 @@ static int zend_jit_trace_handler(zend_jit_ctx *jit, const zend_op_array *op_arr
 static int zend_jit_deoptimizer_start(zend_jit_ctx        *jit,
                                       zend_string         *name,
                                       uint32_t             trace_num,
+                                      zend_jit_trace_info *parent,
                                       uint32_t             exit_num)
 {
 	zend_jit_init_ctx(jit, (zend_jit_vm_kind == ZEND_VM_KIND_CALL) ? 0 : IR_START_BR_TARGET);
@@ -17055,6 +17061,8 @@ static int zend_jit_deoptimizer_start(zend_jit_ctx        *jit,

 	jit->ctx.flags |= IR_SKIP_PROLOGUE;

+	zend_jit_preserve_parent_regs(jit, NULL, parent, exit_num);
+
 	return 1;
 }

@@ -17087,6 +17095,21 @@ static int zend_jit_trace_start(zend_jit_ctx        *jit,
 		jit->ctx.flags |= IR_SKIP_PROLOGUE;
 	}

+	zend_jit_preserve_parent_regs(jit, ssa, parent, exit_num);
+
+	ir_STORE(jit_EG(jit_trace_num), ir_CONST_U32(trace_num));
+
+	return 1;
+}
+
+static void zend_jit_preserve_parent_regs(zend_jit_ctx *jit,
+                                          zend_ssa *ssa,
+                                          zend_jit_trace_info *parent,
+                                          uint32_t exit_num)
+{
+	/* Emit early RLOADs of registers used for deoptimization to prevent
+	 * clobbering. zend_jit_deopt_rload() will reference these. */
+
 	if (parent) {
 		int i;
 		int parent_vars_count = parent->exit_info[exit_num].stack_size;
@@ -17094,7 +17117,6 @@ static int zend_jit_trace_start(zend_jit_ctx        *jit,
 			parent->stack_map +
 			parent->exit_info[exit_num].stack_offset;

-		/* prevent clobbering of registers used for deoptimization */
 		for (i = 0; i < parent_vars_count; i++) {
 			if (STACK_FLAGS(parent_stack, i) != ZREG_CONST
 			 && STACK_REG(parent_stack, i) != ZREG_NONE) {
@@ -17138,10 +17160,6 @@ static int zend_jit_trace_start(zend_jit_ctx        *jit,
 			ir_RLOAD_A(parent->exit_info[exit_num].poly_this.reg);
 		}
 	}
-
-	ir_STORE(jit_EG(jit_trace_num), ir_CONST_U32(trace_num));
-
-	return 1;
 }

 static int zend_jit_trace_begin_loop(zend_jit_ctx *jit)
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index da97d102f20..225257ecd6a 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -7364,7 +7364,7 @@ static const void *zend_jit_trace_exit_to_vm(uint32_t trace_num, uint32_t exit_n

 	name = zend_jit_trace_escape_name(trace_num, exit_num);

-	if (!zend_jit_deoptimizer_start(&ctx, name, trace_num, exit_num)) {
+	if (!zend_jit_deoptimizer_start(&ctx, name, trace_num, &zend_jit_traces[trace_num], exit_num)) {
 		zend_string_release(name);
 		return NULL;
 	}
diff --git a/ext/opcache/tests/jit/gh22915.phpt b/ext/opcache/tests/jit/gh22915.phpt
new file mode 100644
index 00000000000..cea291d311b
--- /dev/null
+++ b/ext/opcache/tests/jit/gh22915.phpt
@@ -0,0 +1,75 @@
+--TEST--
+GH-22915: compiled exit clobbers registers before saving
+--EXTENSIONS--
+opcache
+--INI--
+opcache.jit_max_side_traces=0
+opcache.jit_blacklist_side_trace=0
+--ENV--
+F=iter
+--FILE--
+<?php
+
+final class It implements Iterator {
+    public readonly array $values;
+    public int $position = 0;
+    public function __construct(array $values) {
+        $this->values = $values;
+    }
+
+    public function rewind(): void {}
+
+    public function valid(): bool {
+        return $this->position === 0;
+    }
+
+    public function current(): mixed {
+        if (!isset($this->values[$this->position])) {
+            throw new Exception();
+        }
+
+        return $this->values[$this->position];
+    }
+
+    public function key(): mixed {
+        return $this->position;
+    }
+
+    public function next(): void {
+        $this->position++;
+    }
+}
+
+function iter(It $it) {
+    foreach ($it as $value) {
+        var_dump($value);
+        if (!$value instanceof stdClass) {
+            continue;
+        }
+    }
+}
+
+echo "# First run\n";
+for ($i = 0; $i < 5; $i++) {
+    getenv('F')(new It([getenv('F')]));                // non-immutable, packed array
+}
+
+echo "# Second run\n";
+for ($i = 0; $i < 5; $i++) {
+    getenv('F')(new It([getenv('F'), 'map' => true])); // non-immutable, map, triggers exit
+}
+
+?>
+--EXPECT--
+# First run
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"
+# Second run
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"
+string(4) "iter"