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"