From 985047e8912c53fb6e9bc917f7f56092b466039e Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Thu, 29 Oct 2026 16:20:13 +0100 Subject: [PATCH] Fix GH-24622: Preserve inherited JIT register spill requirements Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com> --- NEWS | 2 + ext/opcache/jit/zend_jit_trace.c | 3 + ext/opcache/tests/jit/gh24622.phpt | 151 +++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 ext/opcache/tests/jit/gh24622.phpt diff --git a/NEWS b/NEWS index 0d4838421d83..3d9ae68fd93b 100644 --- a/NEWS +++ b/NEWS @@ -138,6 +138,8 @@ PHP NEWS with chained Pi nodes). (ndossche) . Fixed JIT treating an array as packed after a string key write turned it into a hash. (jorgsowa) + . Fixed bug GH-24622 (Tracing JIT skips spilling inherited register values + before property helpers). (ndossche, Alessio Attilio) . Fixed bug GH-24436 (Tracing JIT: keep the SEND_VAL type guard after assigning to a reference). (waroad) . Fixed bug GH-24745 (File cache loses attributes on constants on 32-bit diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 4dbdcefef970..950e773f03d6 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -3350,7 +3350,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 000000000000..0c7af19462b8 --- /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-- +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)