Commit bc3a830f946 for php
commit bc3a830f946d2871742f1cacaf1d0504c012110e
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date: Fri Oct 30 21:22:03 2026 +0100
Fix GH-24988: Incorrect opcode range check causes bugs with exceptions in function JIT
`range->var` is a byte offset, but `var_num` is an index, so we have to
convert the index to the byte offset using `EX_NUM_TO_VAR()`.
This matches `cleanup_live_vars()` code.
Closes GH-25070.
Co-authored-by: Jorg Sowa <jorg.sowa@gmail.com>
diff --git a/NEWS b/NEWS
index b73d012c4eb..c8cec45a97e 100644
--- a/NEWS
+++ b/NEWS
@@ -210,6 +210,8 @@ PHP NEWS
when zend_interrupt_function resets EG(vm_interrupt). (Levi Morrison)
. Fixed preloaded functions not JIT-compiled again after OPcache restart.
(Jorg Sowa)
+ . Fixed bug GH-24988 (Incorrect opcode range check causes bugs with exceptions in
+ function JIT). (ndossche, Jorg Sowa)
- OpenSSL:
. Fixed bug GH-24784 (openssl_pkcs7_read() and openssl_cms_read() overwrite
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index 1185ed77e86..53c87ce16da 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -1164,7 +1164,7 @@ static void zend_jit_allocate_registers(zend_jit_ctx *ctx, const zend_op_array *
if (range->start > op_num) {
/* further blocks will not be relevant... */
break;
- } else if (op_num < range->end && var_num == (range->var & ~ZEND_LIVE_MASK)) {
+ } else if (op_num < range->end && EX_NUM_TO_VAR(var_num) == (range->var & ~ZEND_LIVE_MASK)) {
/* check if opcodes in range may throw */
do {
if (zend_may_throw(op_array->opcodes + op_num, ssa->ops + op_num, op_array, ssa)) {
diff --git a/ext/opcache/tests/jit/gh24988.phpt b/ext/opcache/tests/jit/gh24988.phpt
new file mode 100644
index 00000000000..f8cd56e5bec
--- /dev/null
+++ b/ext/opcache/tests/jit/gh24988.phpt
@@ -0,0 +1,47 @@
+--TEST--
+GH-24988 (JIT: crash in cleanup_live_vars when % by zero throws mid-expression)
+--CREDITS--
+waroad
+jorgsowa
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit_buffer_size=64M
+opcache.jit=function
+opcache.optimization_level=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+class D {
+ function self() { return $this; }
+ function __destruct() { echo "dtor\n"; }
+}
+
+// Leaves a stale object in the frame slot deep() uses for its first temporary
+function pollute($a, $b, $c) {
+ return $a->self()->self();
+}
+
+function deep(int $a, int $b, int $c) {
+ $a &= 255;
+ return ($a + 1) - ($b + $b + (($a + 1) % ($b % $c)));
+}
+
+$o = new D;
+var_dump(deep(15, 3, 7));
+pollute($o, 1, 2);
+try {
+ deep(15, 3, 0);
+} catch (DivisionByZeroError $e) {
+ echo $e->getMessage(), "\n";
+}
+echo "end\n";
+
+?>
+--EXPECT--
+int(9)
+Modulo by zero
+end
+dtor