Commit 1a25cf7710b for php

commit 1a25cf7710bb9ffe40e7913449eab5f1014420aa
Author: Alessio Attilio <attilio.alessio@protonmail.com>
Date:   Thu Nov 12 18:56:45 2026 +0000

    Fix GH-24622: Preserve inherited JIT register spill requirements

    Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com>

    Close GH-24642

diff --git a/NEWS b/NEWS
index 6ad2b2ff0f7..bdafdf2ee9c 100644
--- a/NEWS
+++ b/NEWS
@@ -69,6 +69,8 @@ PHP                                                                        NEWS
   . Fixed use-after-free in JIT when the error handler releases a string while
     an "Undefined variable" warning is raised for its offset in ?? or nested
     isset()/empty(). (Jorg Sowa)
+  . Fixed bug GH-24622 (Tracing JIT skips spilling inherited register values
+    before property helpers). (ndossche, Alessio Attilio)

 - PDO_Firebird:
   . Fixed stack buffer overflow with long connection user names, passwords,
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index b7f98764649..37f3f9aecb1 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -3351,7 +3351,10 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t
 		} else if (p->stop >= ZEND_JIT_TRACE_STOP_LINK) {
 			for (i = 0; i < op_array->last_var + op_array->T; i++) {
 				int var = STACK_VAR(stack, i);
+				/* A side trace must define the variable, otherwise it may set ZREG_STORE on
+				 * variables inherited from the parent trace that are only stored when linking. */
 				if (var >= 0 && RA_HAS_REG(var)
+				 && ssa->vars[var].definition >= 0
 				 && !(RA_REG_FLAGS(var) & (ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE))) {
 					RA_REG_FLAGS(var) |= ZREG_STORE;
 				}
diff --git a/ext/opcache/tests/jit/gh24622.phpt b/ext/opcache/tests/jit/gh24622.phpt
new file mode 100644
index 00000000000..0c7af19462b
--- /dev/null
+++ b/ext/opcache/tests/jit/gh24622.phpt
@@ -0,0 +1,151 @@
+--TEST--
+GH-24622: Tracing JIT must spill inherited register values before property helpers
+--EXTENSIONS--
+opcache
+spl
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit_buffer_size=64M
+opcache.jit=tracing
+zend.assertions=1
+assert.exception=1
+--CREDITS--
+frodeborli
+--FILE--
+<?php
+
+final class StringBuffer
+{
+    private \SplDoublyLinkedList $queue;
+    private string $buffer = '';
+    private int $length = 0;
+    private int $offset = 0;
+    private int $totalRead = 0;
+    private int $totalWritten = 0;
+    private bool $ended = false;
+
+    public function __construct()
+    {
+        $this->queue = new \SplDoublyLinkedList();
+    }
+
+    public function isEmpty(): bool
+    {
+        return $this->offset === $this->length && $this->queue->isEmpty();
+    }
+
+    public function write(string $chunk): void
+    {
+        if ($this->ended) {
+            throw new \RuntimeException('Buffer has been ended');
+        }
+        $this->totalWritten += \strlen($chunk);
+        $this->queue->push($chunk);
+    }
+
+    public function read(int $maxLength): string
+    {
+        if ($maxLength < 0) {
+            throw new \OutOfBoundsException("Can't read negative lengths");
+        }
+        $this->fill($maxLength);
+        $chunk = \substr($this->buffer, $this->offset, $maxLength);
+        $length = \strlen($chunk);
+        $this->offset += $length;
+        $this->totalRead += $length;
+
+        return $chunk;
+    }
+
+    public function end(): void
+    {
+        if ($this->ended) {
+            throw new \LogicException('StringBuffer already ended');
+        }
+        $this->ended = true;
+    }
+
+    public function eof(): bool
+    {
+        return $this->ended && $this->offset === $this->length && $this->queue->isEmpty();
+    }
+
+    public function unread(string $chunk): void
+    {
+        if ($this->ended && $this->offset === $this->length && $this->queue->isEmpty()) {
+            throw new \LogicException("Can't unread to an ended and empty StringBuffer");
+        }
+        $chunkLength = \strlen($chunk);
+        $this->totalRead -= $chunkLength;
+        if ($this->length === $this->offset) {
+            $this->queue->unshift($chunk);
+        } else {
+            $this->buffer = $chunk . \substr($this->buffer, $this->offset);
+            $this->length += $chunkLength - $this->offset;
+            $this->offset = 0;
+        }
+    }
+
+    protected function fill(int $requiredLength): bool
+    {
+        if ($this->offset > 4096) {
+            $this->buffer = \substr($this->buffer, $this->offset);
+            $this->length -= $this->offset;
+            $this->offset = 0;
+        }
+        while ($this->length < $this->offset + $requiredLength && !$this->queue->isEmpty()) {
+            $chunk       = $this->queue->shift();
+            $chunkLength = \strlen($chunk);
+
+            if ($this->length === $this->offset) {
+                $this->buffer = $chunk;
+                $this->length = $chunkLength;
+                $this->offset = 0;
+            } else {
+                $this->buffer .= $chunk;
+                $this->length += $chunkLength;
+            }
+        }
+
+        return $this->length >= $this->offset + $requiredLength;
+    }
+}
+
+$sb = new StringBuffer();
+$count = 0;
+for ($i = 0; $i < 1000; ++$i) { $sb->write('x'); }
+$sb->end();
+while (!$sb->eof()) { if ('' !== $sb->read(1)) { ++$count; } }
+assert($count === 1000);
+
+$sb = new StringBuffer();
+$totalReceived = 0;
+$chunk = str_repeat('x', 1024);
+for ($i = 0; $i < 100; ++$i) { $sb->write($chunk); }
+$sb->end();
+while (!$sb->eof()) { $totalReceived += strlen($sb->read(8192)); }
+assert($totalReceived === 102400);
+
+$sb = new StringBuffer();
+assert($sb->eof() === false);
+$sb->write('Hello');
+assert($sb->read(10000) === 'Hello');
+assert($sb->read(100) === '');
+assert($sb->write('ab') === null);
+assert($sb->read(1) === 'a');
+assert($sb->read(2) === 'b');
+$sb->write('abc');
+assert($sb->eof() === false);
+$sb->write('def');
+$sb->end();
+$sb->unread('b');
+assert($sb->read(5) === 'babcd');
+var_dump($sb->eof());
+assert($sb->read(10000) === 'ef');
+var_dump($sb->eof());
+
+--EXPECT--
+bool(false)
+bool(true)