Commit 80dc4c19d65 for php.net

commit 80dc4c19d65e9f8559c37620fa506a49ef46ed5e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Wed Mar 11 08:22:50 2026 -0400

    Fix GH-20838: JIT compiler produces wrong arithmetic results (#21383)

    Insert type guards (CHECK_OP1_TRACE_TYPE / CHECK_OP2_TRACE_TYPE) on the
    sensitive bailout paths in ADD/SUB/MUL JIT compilation: the MAY_BE_UNDEF
    and non-numeric operand breaks. Guards are only emitted when the traced
    operand type is IS_LONG or IS_DOUBLE, ensuring TSSA result type
    predictions stay valid for side traces without affecting the normal
    numeric fast path.


    Fixes GH-20838

    Co-authored-by: Dmitry Stogov <dmitrystogov@gmail.com>

diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 9d2de55e294..03b59eea615 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -4510,6 +4510,12 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par
 						op2_info = OP2_INFO();
 						op2_addr = OP2_REG_ADDR();
 						if ((op1_info & MAY_BE_UNDEF) || (op2_info & MAY_BE_UNDEF)) {
+							if (op1_type == IS_LONG || op1_type == IS_DOUBLE) {
+								CHECK_OP1_TRACE_TYPE();
+							}
+							if (op2_type == IS_LONG || op2_type == IS_DOUBLE) {
+								CHECK_OP2_TRACE_TYPE();
+							}
 							break;
 						}
 						if (opline->opcode == ZEND_ADD &&
@@ -4518,6 +4524,12 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par
 							/* pass */
 						} else if (!(op1_info & (MAY_BE_LONG|MAY_BE_DOUBLE)) ||
 						    !(op2_info & (MAY_BE_LONG|MAY_BE_DOUBLE))) {
+							if (op1_type == IS_LONG || op1_type == IS_DOUBLE) {
+								CHECK_OP1_TRACE_TYPE();
+							}
+							if (op2_type == IS_LONG || op2_type == IS_DOUBLE) {
+								CHECK_OP2_TRACE_TYPE();
+							}
 							break;
 						}
 						if (orig_op1_type != IS_UNKNOWN
diff --git a/ext/opcache/tests/jit/gh20838.inc b/ext/opcache/tests/jit/gh20838.inc
new file mode 100644
index 00000000000..997cc23b8fe
--- /dev/null
+++ b/ext/opcache/tests/jit/gh20838.inc
@@ -0,0 +1,53 @@
+<?php
+$result = json_decode('[{"ID":"49"},{"ID":"1080"},{"ID":"4415"},{"ID":"4763"},{"ID":"4926"},{"ID":"4933"},{"ID":"4935"},{"ID":"4938"},{"ID":"4939"},{"ID":"4947"},{"ID":"4952"},{"ID":"4953"},{"ID":"4962"},{"ID":"8558"},{"ID":"14888"},{"ID":"16879"},{"ID":"100003"},{"ID":"100007"},{"ID":"100013"},{"ID":"100017"},{"ID":"100019"},{"ID":"100027"},{"ID":"100031"},{"ID":"106427"},{"ID":"217955"},{"ID":"240000"},{"ID":"240010"},{"ID":"240021"},{"ID":"240079"},{"ID":"240210"},{"ID":"240227"},{"ID":"240249"},{"ID":"240273"},{"ID":"240333"},{"ID":"240335"},{"ID":"300024"},{"ID":"310001"},{"ID":"310025"},{"ID":"310034"},{"ID":"310042"},{"ID":"310111"},{"ID":"310191"},{"ID":"310219"},{"ID":"310236"},{"ID":"310322"},{"ID":"310356"},{"ID":"310360"},{"ID":"310394"},{"ID":"310405"},{"ID":"310411"}]', true);
+
+$result1 = json_decode('[{"ID":"1080","cat":"6","vote":"2"},{"ID":"4415","cat":"1","vote":"5"},{"ID":"4415","cat":"5","vote":"-5"},{"ID":"4763","cat":"1","vote":"5"},{"ID":"4926","cat":"1","vote":"5"},{"ID":"4933","cat":"1","vote":"4"},{"ID":"4935","cat":"1","vote":"5"},{"ID":"4938","cat":"1","vote":"5"},{"ID":"4939","cat":"1","vote":"5"},{"ID":"4947","cat":"1","vote":"4"},{"ID":"4952","cat":"1","vote":"3"},{"ID":"4953","cat":"1","vote":"5"},{"ID":"4962","cat":"1","vote":"4"},{"ID":"8558","cat":"1","vote":"3"},{"ID":"14888","cat":"4","vote":"5"},{"ID":"16879","cat":"4","vote":"-2"},{"ID":"16879","cat":"6","vote":"3"},{"ID":"100003","cat":"1","vote":"0"},{"ID":"100007","cat":"1","vote":"4"},{"ID":"100013","cat":"1","vote":"0"},{"ID":"100017","cat":"6","vote":"4"},{"ID":"100017","cat":"4","vote":"-4"},{"ID":"100019","cat":"1","vote":"1"},{"ID":"100027","cat":"1","vote":"3"},{"ID":"100031","cat":"1","vote":"2"},{"ID":"106427","cat":"5","vote":"-5"},{"ID":"217955","cat":"4","vote":"-1"},{"ID":"217955","cat":"4","vote":"-1"},{"ID":"240000","cat":"1","vote":"2"},{"ID":"240010","cat":"1","vote":"4"},{"ID":"240021","cat":"1","vote":"3"},{"ID":"240079","cat":"1","vote":"4"},{"ID":"240079","cat":"7","vote":"4"},{"ID":"240079","cat":"6","vote":"2"},{"ID":"240210","cat":"1","vote":"3"},{"ID":"240227","cat":"1","vote":"3"},{"ID":"240249","cat":"1","vote":"5"},{"ID":"240273","cat":"1","vote":"5"},{"ID":"240333","cat":"1","vote":"2"},{"ID":"240335","cat":"1","vote":"4"},{"ID":"300024","cat":"3","vote":"4"},{"ID":"300024","cat":"1","vote":"5"},{"ID":"310001","cat":"1","vote":"3"},{"ID":"310025","cat":"1","vote":"3"},{"ID":"310034","cat":"1","vote":"4"},{"ID":"310042","cat":"1","vote":"2"},{"ID":"310111","cat":"1","vote":"5"},{"ID":"310191","cat":"1","vote":"3"},{"ID":"310219","cat":"1","vote":"5"},{"ID":"310236","cat":"1","vote":"1"},{"ID":"310322","cat":"1","vote":"3"},{"ID":"310356","cat":"1","vote":"2"},{"ID":"310360","cat":"1","vote":"3"},{"ID":"310360","cat":"4","vote":"-5"},{"ID":"310394","cat":"1","vote":"1"},{"ID":"310394","cat":"3","vote":"2"},{"ID":"310405","cat":"1","vote":"5"},{"ID":"310411","cat":"1","vote":"4"}]', true);
+
+foreach ($result as $arr) {
+    $v = [];
+    foreach ($result1 as $arr1) {
+        if ($arr1['ID'] == $arr['ID']) $v[] = $arr1;
+    }
+    TLVotesStatUpdateBAD($arr['ID'], $v);
+}
+
+function TLVotesStatUpdateBAD(int $id, ?array $v = null): float
+{
+    static $k;
+    if (!isset($k)) $k = json_decode('{"4":{"tp":"20","st":"sum"},"3":{"tp":"30","st":"caa"},"2":{"tp":"60","st":"sum"},"1":{"tp":"50","st":"unique"},"5":{"tp":"50","st":"caa"},"6":{"tp":"20","st":"caa"},"7":{"tp":"60","st":"caa"},"8":{"tp":"60","st":"caa"},"255":{"tp":"0","st":"correction"}}', true);
+
+    $pav = 50;
+    $s = 0.0;
+    $corrections = 0;
+    $av = [];
+    $avc = [];
+
+    foreach ($v as $arr) {
+        if ($k[$arr['cat']]['st'] == 'unique') $av[$arr['cat']] = $arr['vote'];
+        elseif ($k[$arr['cat']]['st'] == 'correction') {
+            $av[$arr['cat']] = $arr['vote'];
+            $corrections++;
+        } else {
+            if ($arr['vote'] < 0) $arr['vote'] = $arr['vote'] / 2;
+            $av[$arr['cat']] ??= 0;
+            $avc[$arr['cat']] ??= 0;
+            $av[$arr['cat']] += $arr['vote'];
+            $avc[$arr['cat']]++;
+        }
+    }
+
+    if (($c = count($av))) {
+        $c -= $corrections;
+        foreach ($av as $key => $value) {
+            if ($k[$key]['st'] == 'correction') $s += $value;
+            else {
+                if ($k[$key]['st'] == 'caa') $value = $value / $avc[$key];
+                $s += $value / 5 * $k[$key]['tp'] * (1 + 50 / $k[$key]['tp'] * $pav / (100 * $c));
+            }
+        }
+    }
+
+    echo("$id $s\n");
+
+    return $s;
+}
diff --git a/ext/opcache/tests/jit/gh20838.phpt b/ext/opcache/tests/jit/gh20838.phpt
new file mode 100644
index 00000000000..bccf0a0ed8c
--- /dev/null
+++ b/ext/opcache/tests/jit/gh20838.phpt
@@ -0,0 +1,66 @@
+--TEST--
+GH-20838 (JIT compiler produces wrong arithmetic results)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=tracing
+opcache.jit_buffer_size=64M
+opcache.jit_hot_loop=61
+opcache.jit_hot_func=127
+opcache.jit_hot_return=8
+opcache.jit_hot_side_exit=8
+--EXTENSIONS--
+opcache
+--FILE_EXTERNAL--
+gh20838.inc
+--EXPECT--
+49 0
+1080 18
+4415 31.25
+4763 75
+4926 75
+4933 60
+4935 75
+4938 75
+4939 75
+4947 60
+4952 45
+4953 75
+4962 60
+8558 45
+14888 45
+16879 13
+100003 0
+100007 60
+100013 0
+100017 13
+100019 15
+100027 45
+100031 30
+106427 -37.5
+217955 -9
+240000 30
+240010 60
+240021 45
+240079 112.66666666667
+240210 45
+240227 45
+240249 75
+240273 75
+240333 30
+240335 60
+300024 96.5
+310001 45
+310025 45
+310034 60
+310042 30
+310111 75
+310191 45
+310219 75
+310236 15
+310322 45
+310356 30
+310360 21.25
+310394 29.5
+310405 75
+310411 60