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)