Commit 7f8c655ecca for php
commit 7f8c655eccab70d038adf75e1a3f2c56fe315040
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Nov 3 09:26:00 2026 -0500
Fix GH-25165: Tracing JIT spins once the JIT buffer is full
When a side trace cannot be compiled because the JIT buffer is full,
zend_jit_blacklist_trace_exit() marks the exit blacklisted without linking
it. Every later hit of that exit returned 0 from zend_jit_trace_exit() and
resumed at EX(opline); when that is the root trace's start opline, its
handler is the trace itself, so execution re-entered the trace and took the
same exit forever. Return 1 in that case so the original handler runs, as
zend_jit_trace_exit_to_vm() already does when it can emit an exit stub.
Also backport IR commit 43d1d1023a6ea811b11b39ed738f5e18696a1254, which
frees the DynASM state when the code buffer is full.
Fixes GH-25165
Closes GH-25182
diff --git a/NEWS b/NEWS
index 41ba7e3ff87..f42114aa991 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,10 @@ PHP NEWS
- Curl:
. Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
+- Opcache:
+ . Fixed bug GH-25165 (Tracing JIT loops forever once opcache.jit_buffer_size
+ is exhausted while compiling side traces). (Ilia Alshanetsky)
+
19 Nov 2026, PHP 8.4.28
- Core:
diff --git a/ext/opcache/jit/ir/ir_aarch64.dasc b/ext/opcache/jit/ir/ir_aarch64.dasc
index dae5837403d..c3e88e46e3b 100644
--- a/ext/opcache/jit/ir/ir_aarch64.dasc
+++ b/ext/opcache/jit/ir/ir_aarch64.dasc
@@ -6915,6 +6915,7 @@ void *ir_emit_code(ir_ctx *ctx, size_t *size_ptr)
entry = ctx->code_buffer->pos;
entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
if (size > (size_t)((char*)ctx->code_buffer->end - (char*)entry)) {
+ dasm_free(&data.dasm_state);
ctx->data = NULL;
ctx->status = IR_ERROR_CODE_MEM_OVERFLOW;
return NULL;
@@ -7057,6 +7058,7 @@ const void *ir_emit_exitgroup(uint32_t first_exit_point, uint32_t exit_points_pe
entry = code_buffer->pos;
entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
if (size > (size_t)((char*)code_buffer->end - (char*)entry)) {
+ dasm_free(&dasm_state);
return NULL;
}
code_buffer->pos = (char*)entry + size;
diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc
index a62e49fc40e..dee5867108e 100644
--- a/ext/opcache/jit/ir/ir_x86.dasc
+++ b/ext/opcache/jit/ir/ir_x86.dasc
@@ -12244,6 +12244,7 @@ next_block:;
entry = ctx->code_buffer->pos;
entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
if (size > (size_t)((char*)ctx->code_buffer->end - (char*)entry)) {
+ dasm_free(&data.dasm_state);
ctx->data = NULL;
ctx->status = IR_ERROR_CODE_MEM_OVERFLOW;
return NULL;
@@ -12383,6 +12384,7 @@ const void *ir_emit_exitgroup(uint32_t first_exit_point, uint32_t exit_points_pe
entry = code_buffer->pos;
entry = (void*)IR_ALIGNED_SIZE(((size_t)(entry)), 16);
if (size > (size_t)((char*)code_buffer->end - (char*)entry)) {
+ dasm_free(&dasm_state);
return NULL;
}
code_buffer->pos = (char*)entry + size;
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index b424934e492..9c54767fc22 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -8765,7 +8765,7 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf
return 1;
/* Lock-free check if the side trace was already JIT-ed or blacklist-ed in another process */
} else if (t->exit_info[exit_num].flags & (ZEND_JIT_EXIT_JITED|ZEND_JIT_EXIT_BLACKLISTED)) {
- return 0;
+ return EX(opline) == zend_jit_traces[t->root].opline;
}
ZEND_ASSERT(EX(func)->type == ZEND_USER_FUNCTION);
diff --git a/ext/opcache/tests/jit/gh25165.phpt b/ext/opcache/tests/jit/gh25165.phpt
new file mode 100644
index 00000000000..2315243c75e
--- /dev/null
+++ b/ext/opcache/tests/jit/gh25165.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-25165 (Tracing JIT: infinite loop once opcache.jit_buffer_size is exhausted while compiling side traces)
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=tracing
+opcache.jit_buffer_size=4K
+opcache.jit_hot_loop=1
+opcache.jit_hot_func=1
+opcache.jit_hot_return=1
+opcache.jit_hot_side_exit=1
+--FILE--
+<?php
+function f0($a) { $x = $a; $i = 0; do { $x = $x + 0; } while (++$i < 4); return $x; }
+function f1($a) { $x = $a; $i = 0; do { $x = $x + 1; } while (++$i < 4); return $x; }
+function f2($a) { $x = $a; $i = 0; do { $x = $x + 2; } while (++$i < 4); return $x; }
+function f3($a) { $x = $a; $i = 0; do { $x = $x + 3; } while (++$i < 4); return $x; }
+function f4($a) { $x = $a; $i = 0; do { $x = $x + 4; } while (++$i < 4); return $x; }
+function f5($a) { $x = $a; $i = 0; do { $x = $x + 5; } while (++$i < 4); return $x; }
+function f6($a) { $x = $a; $i = 0; do { $x = $x + 6; } while (++$i < 4); return $x; }
+function f7($a) { $x = $a; $i = 0; do { $x = $x + 7; } while (++$i < 4); return $x; }
+function f8($a) { $x = $a; $i = 0; do { $x = $x + 8; } while (++$i < 4); return $x; }
+function f9($a) { $x = $a; $i = 0; do { $x = $x + 9; } while (++$i < 4); return $x; }
+
+$sum = 0;
+foreach ([1, '1', 1.5, 2] as $v) {
+ for ($j = 0; $j < 300; $j++) {
+ $sum += f0($v) + f1($v) + f2($v) + f3($v) + f4($v)
+ + f5($v) + f6($v) + f7($v) + f8($v) + f9($v);
+ }
+}
+var_dump($sum);
+?>
+--EXPECT--
+float(232500)