<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://endive.run/blog</id>
    <title>Endive Blog</title>
    <updated>2026-05-21T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://endive.run/blog"/>
    <subtitle>Endive Blog</subtitle>
    <icon>https://endive.run/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Finding a JVM JIT Bug the Hard Way]]></title>
        <id>https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way</id>
        <link href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way"/>
        <updated>2026-05-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Finding a JVM JIT Bug]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="Finding a JVM JIT Bug" src="https://endive.run/assets/images/finding-a-jvm-jit-bug-5f208171622a3a77b50ad4bdfb0d88a8.png" width="960" height="640" class="img_ev3q"></p>
<p>In early 2025, a user reported intermittent wrong results using the build-time compiler. Not a crash, just a quietly wrong answer. A year of debugging, dead ends, and creative workarounds later, we traced the problem to a bug in the JVM's C2 JIT compiler, and had to build an entire compiler from scratch in 3 days just to prove it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-wrong-answer">The wrong answer<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#the-wrong-answer" class="hash-link" aria-label="Direct link to The wrong answer" title="Direct link to The wrong answer" translate="no">​</a></h2>
<p>In February 2025, a user opened <a href="https://github.com/dylibso/chicory/issues/755" target="_blank" rel="noopener noreferrer" class="">an issue</a> against the project. They were running a large Wasm module, built from a C++ codebase, on the JVM, and something was wrong: an operation that reverses a string was silently producing incorrect results, though not consistently. It only happened on Java 17 Temurin, only with the build-time compiler, and only when the Wasm module was large enough.</p>
<p>The key word is <em>silently</em>. There was no exception and no segfault, just a comparison operation deep inside the compiled code returning the wrong boolean while everything downstream acted on that corrupted result. Correctness bugs are the most dangerous kind: crashes tell you something is wrong; wrong answers let you ship broken software with confidence.</p>
<p>Before you worry: this bug requires an extraordinary set of conditions to trigger. If you're writing ordinary Java applications, the odds of encountering it are vanishingly small. But the story of how we found, worked around, and finally fixed it is worth telling.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="not-our-bug">Not our bug<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#not-our-bug" class="hash-link" aria-label="Direct link to Not our bug" title="Direct link to Not our bug" translate="no">​</a></h2>
<p><a href="https://github.com/evacchi" target="_blank" rel="noopener noreferrer" class="">Edoardo Vacchi</a> and I paired on the investigation with the reported module. We narrowed the problem to the implementation of a single WebAssembly opcode: <code>I32_GE_U</code>, the unsigned greater-than-or-equal comparison. Here's the entire implementation:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">I32_GE_U</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token class-name">Integer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">compareUnsigned</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">TRUE</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">FALSE</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>One line. <code>Integer.compareUnsigned</code> is a standard JDK method that has existed since Java 8, and it is a platform intrinsic, meaning the JIT compiler replaces it with optimized machine code rather than executing the Java implementation. There is no bug in this code.</p>
<p>But when we added a single JVM flag, <code>-XX:CompileCommand=dontinline,com/dylibso/chicory/runtime/OpcodeImpl.I32_GE_U</code>, the problem vanished. That flag tells the C2 JIT compiler not to inline the method. The fact that preventing inlining fixed the issue pointed the finger squarely at how C2 <em>optimizes</em> the inlined code, not at our logic.</p>
<p>This was our introduction to a genuine <a href="https://en.wikipedia.org/wiki/Heisenbug" target="_blank" rel="noopener noreferrer" class="">Heisenbug</a>: a bug whose behavior changes when you try to observe it. Adding a <code>System.out.println</code> inside the method made it vanish because the print statement changes C2's inlining decisions. Attaching a debugger had the same effect since it prevents certain JIT optimizations. Even reducing the Wasm module to a smaller test case killed the reproduction, because the JIT needs approximately 45 million function calls to build up enough type profile information to trigger the aggressive optimization that contains the defect.</p>
<p>Every tool in a debugger's standard toolkit made the problem disappear.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="living-with-the-bug">Living with the bug<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#living-with-the-bug" class="hash-link" aria-label="Direct link to Living with the bug" title="Direct link to Living with the bug" translate="no">​</a></h2>
<p>We couldn't fix the JVM, and we couldn't wait for a fix we didn't yet have. So we got creative with workarounds.</p>
<p>The first approach, suggested by <a href="https://github.com/chirino" target="_blank" rel="noopener noreferrer" class="">Hiram Chirino</a>, was a <a href="https://github.com/dylibso/chicory/pull/844" target="_blank" rel="noopener noreferrer" class="">megamorphic dispatch trick</a>. The idea: if C2 only miscompiles the code when it inlines through a monomorphic call site, we can force C2 to give up on inlining by making the call site look polymorphic. At static initialization time, we cycle through different lambda implementations of the same interface, training the JIT to see the call site as megamorphic (too many receiver types to optimize):</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">I32GEUFunc</span><span class="token plain"> noop1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">I32GEUFunc</span><span class="token plain"> noop2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        i32geuFunc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> noop1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">MemCopyWorkaround</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">i32_ge_u</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        i32geuFunc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> noop2</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name">MemCopyWorkaround</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">i32_ge_u</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    i32geuFunc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-&gt;</span><span class="token plain"> </span><span class="token class-name">OpcodeImpl</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">I32_GE_U</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>It's an ugly hack, but it keeps the user mostly safe. We <a href="https://github.com/dylibso/chicory/pull/1178" target="_blank" rel="noopener noreferrer" class="">extended it</a> to cover all affected code paths and added a CI test using a large Wasm module to catch regressions. The workaround only activates on Java 17 and earlier.</p>
<p>There's an irony here: on Java 18+, the bug was already masked. An <a href="https://github.com/openjdk/jdk/pull/6101" target="_blank" rel="noopener noreferrer" class="">entirely unrelated optimization</a> by <a href="https://github.com/merykitty" target="_blank" rel="noopener noreferrer" class="">Quan Anh Mai</a> in November 2021 (JDK-8276162, "Optimise unsigned comparison pattern") taught C2 to recognize the <code>Integer.compareUnsigned</code> pattern earlier in the pipeline and lower it directly to an unsigned machine comparison. With that optimization in place, by the time C2 reaches the buggy if-folding stage, the signed comparison pattern it would have miscompiled no longer exists. The bug is still there, it just never fires because its input has already been optimized away.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-reproducer-problem">The reproducer problem<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#the-reproducer-problem" class="hash-link" aria-label="Direct link to The reproducer problem" title="Direct link to The reproducer problem" translate="no">​</a></h2>
<p>The workarounds bought us time, but they weren't a fix. I had a bug in CI, workarounds in production, and a deep suspicion about what was going wrong inside C2. What I didn't have was a way to prove it to the OpenJDK team.</p>
<p>The OpenJDK project reasonably requires Java source code reproducers with bug reports. They need something that compiles with <code>javac</code>, runs with standard JDK tools, and demonstrates the problem in isolation. But our build-time compiler generates JVM bytecode directly from Wasm, freely using <code>goto</code> and labels in patterns that have no direct Java source representation. You can't just decompile it.</p>
<p>I tried. Every major decompiler (CFR, Procyon, Fernflower) either choked on the input size (the generated bytecode file was enormous) or produced non-compilable output, with issues especially around control flow in nested blocks. I also tried <a href="https://cr.openjdk.org/~thartmann/talks/2020-Debugging_HotSpot.pdf" target="_blank" rel="noopener noreferrer" class="">HotSpot's replay compilation</a>: I could reproduce the bug during replay, but the output didn't give enough insight to isolate the root cause.</p>
<p>The only consistent reproducer we had was <a href="https://github.com/WebAssembly/wabt" target="_blank" rel="noopener noreferrer" class="">wat2wasm</a>, a tool from the <a href="https://github.com/WebAssembly/wabt" target="_blank" rel="noopener noreferrer" class="">WebAssembly Binary Toolkit (WABT)</a> that converts WebAssembly text format to binary format, roughly 195,000 lines of C++ compiled to a single large Wasm module. I tried reducing it. The original generated code was over 213,000 lines of Java. I managed to get the generated code down to about 40,000 lines, but I couldn't go further. The bug required all 294 functions in the module to be present, approximately 45 million function calls to build C2's type profiles, and specific bytecode patterns that emerged only from the full module. Removing any piece meant C2 would never reach the optimization threshold.</p>
<p>I spent months chasing down the issue. Multiple branches in <a href="https://github.com/andreaTP/chicory" target="_blank" rel="noopener noreferrer" class="">my fork</a> (<code>decompiling-attempt1</code>, <code>JDK-8376400-reproducer</code>, and others) document the dead ends. Every approach that should have worked didn't. The Heisenbug lived up to its name.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="building-a-compiler-to-catch-a-compiler-bug">Building a compiler to catch a compiler bug<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#building-a-compiler-to-catch-a-compiler-bug" class="hash-link" aria-label="Direct link to Building a compiler to catch a compiler bug" title="Direct link to Building a compiler to catch a compiler bug" translate="no">​</a></h2>
<p>By February 2026, I had the bug on my mind for nearly a year. The megamorphic workaround was holding enough, but the real fix was sitting behind a wall I couldn't climb: I needed Java source code, and the toolchain produced Java bytecode.</p>
<p>What if I built a different toolchain?</p>
<p>Instead of trying to decompile bytecode back into Java sources, what if I compiled WebAssembly directly to Java source code, skipping bytecode entirely? The generated Java would be verbose and mechanical, but it would be compilable with <code>javac</code>. It was a bet: if <code>javac</code> happened to produce bytecode with the same patterns that triggered the C2 bug, I'd have my reproducer.</p>
<p>The idea was clear, but the scope was daunting. A source compiler that could handle something the size of <code>wat2wasm</code> is a real compiler: it needs to handle over a hundred opcodes, translate WebAssembly's structured control flow (blocks, loops, <code>br_if</code>, <code>br_table</code>) into Java's <code>while</code>/<code>switch</code>/<code>break</code> constructs, split methods to stay under the JVM's 64KB method size limit, and handle edge cases in memory operations, type conversions, and stack manipulation.</p>
<p>I wouldn't have attempted it without a machine helping me.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-days-later">3 days later<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#3-days-later" class="hash-link" aria-label="Direct link to 3 days later" title="Direct link to 3 days later" translate="no">​</a></h3>
<p>In February 2026, I started using <a href="https://claude.ai/" target="_blank" rel="noopener noreferrer" class="">Claude</a> and wanted to test it on something that was both low-risk (a standalone, one shot tool, not a change to the runtime) and hard enough to be a meaningful test. Building a Wasm-to-Java-source compiler fit perfectly.</p>
<p>The approach was informally spec-driven from the start. We already had a test suite generator that creates Java test cases from the official <a href="https://github.com/WebAssembly/testsuite" target="_blank" rel="noopener noreferrer" class="">WebAssembly spec test suite</a>. I hooked the source compiler into that generator on day one, so every opcode implementation was validated against the spec as it was written. From there, I used real-world Wasm modules and the WASI testsuite to catch edge cases the spec tests didn't cover.</p>
<p>Claude handled the mechanical work: porting opcodes one by one following established patterns, generating the boilerplate for type conversions, and iterating through the hundreds of Wasm instructions that all follow similar structures. I steered the architecture: the control flow translation strategy, the method splitting heuristic to stay under the JVM's 64KB method limit, and debugging the edge cases where WebAssembly's semantics diverge from Java's.</p>
<p>The spec test suite provided continuous validation: if a change broke something, we knew immediately which opcode and which test case failed. This tight feedback loop made it possible to move fast without accumulating hidden bugs or making regressions.</p>
<p>In a little more than 3 days of very hard work, I went from zero to a compiler that passed over 25,000 spec tests and all WASI tests. Without the LLM, I would never have started: the effort of writing a full source compiler solo would have been hard to justify against the uncertain payoff. With it, the turnaround was fast enough that building the tool was worth more than spending another month trying to reduce the bytecode reproducer.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-moment-of-truth">The moment of truth<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#the-moment-of-truth" class="hash-link" aria-label="Direct link to The moment of truth" title="Direct link to The moment of truth" translate="no">​</a></h3>
<p>Compiling <code>wat2wasm</code> through the source compiler produced approximately 200,000 lines of Java source code.</p>
<p>When I compiled that Java source with <code>javac</code>, ran it on Java 17, and fed it a large enough <code>.wat</code> input, the program produced wrong results. Same <code>out of bounds memory access</code> error, same non-deterministic behavior, same Heisenbug, now reproduced in pure Java source code. 🎉</p>
<p>I finally had what the OpenJDK team needed: a self-contained, <code>javac</code>-compilable reproducer of a C2 JIT compiler bug. <a href="https://github.com/dylibso/chicory/pull/1200" target="_blank" rel="noopener noreferrer" class="">PR #1200</a> documents the source compiler and the reproducer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-20-line-fix">The 20-line fix<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#the-20-line-fix" class="hash-link" aria-label="Direct link to The 20-line fix" title="Direct link to The 20-line fix" translate="no">​</a></h2>
<p>With the 200,000-line reproducer in hand, <a href="https://github.com/rwestrel" target="_blank" rel="noopener noreferrer" class="">Roland Westrelin</a> took a look. He did what I couldn't: he understood the C2 optimization pipeline well enough to distill our massive reproducer into a <a href="https://github.com/openjdk/jdk/pull/30677" target="_blank" rel="noopener noreferrer" class="">20-line test case</a>:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">void</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">test1</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">int</span><span class="token plain"> v</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// (1) C2 sees this signed comparison</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">i </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">MIN_VALUE</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token class-name">Integer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MIN_VALUE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        v </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// (2) Uncommon trap, C2 assumes this is never taken</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">v </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RuntimeException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"never taken"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// (3) Second signed comparison, C2 folds (1) and (3)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">//     into a single unsigned comparison</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">i </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">MIN_VALUE</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token class-name">Integer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">MIN_VALUE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        taken1</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The bug lives in the interaction of three C2 optimizations:</p>
<ol>
<li class="">
<p><strong>Signed comparisons with uncommon traps.</strong> C2 compiles the two <code>if</code> statements and records that the <code>throw</code> branch is never taken (an "uncommon trap", a deoptimization point for rare paths).</p>
</li>
<li class="">
<p><strong>Split-if.</strong> C2's <code>do_split_if()</code> optimization duplicates control flow through a Phi node, specializing each branch. This modifies the state captured by the uncommon trap.</p>
</li>
<li class="">
<p><strong>If-folding.</strong> <code>IfNode::fold_compares()</code> combines the two signed comparisons into a single, more efficient unsigned comparison. But it doesn't know that split-if has already modified the uncommon trap's saved state. When deoptimization fires at runtime, execution resumes at the wrong bytecode point with corrupted state.</p>
</li>
</ol>
<p>The fix is surgical: a <code>_safe_for_fold_compare</code> flag. When <code>do_split_if()</code> modifies an uncommon trap, it marks it as unsafe for if-folding. The fold_compares optimization checks this flag before proceeding. 369 additions (mostly tests), 3 deletions. The fix was <a href="https://github.com/openjdk/jdk/pull/30677" target="_blank" rel="noopener noreferrer" class="">integrated</a> on April 30, 2026.</p>
<p>As one of the reviewers noted: <em>"these kind of bugs with wrong safepoint states are really hard to catch."</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="should-you-worry">Should you worry?<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#should-you-worry" class="hash-link" aria-label="Direct link to Should you worry?" title="Direct link to Should you worry?" translate="no">​</a></h2>
<p>No.</p>
<p>This bug requires a very specific convergence of conditions:</p>
<ul>
<li class=""><strong>A particular code pattern</strong>: multiple signed integer comparisons that C2 can fold into unsigned comparisons, with uncommon traps that the split-if optimization modifies. This pattern is rare in handwritten Java.</li>
<li class=""><strong>Enormous JIT warmup</strong>: approximately 45 million function calls to build the type profile data that triggers C2's most aggressive optimizations. Most Java methods never reach this threshold.</li>
<li class=""><strong>JDK 11 through 17</strong>: on JDK 18 and later, an unrelated optimization (JDK-8276162) masks the bug entirely.</li>
<li class=""><strong>Generated code patterns</strong>: the comparison-heavy, uniform-dispatch patterns that WebAssembly runtimes produce are unusual. Handwritten Java code tends toward more diverse control flow that doesn't trigger the specific optimization sequence.</li>
</ul>
<p>We hit these conditions because we are aggressively leveraging C2's inlining and JIT optimizations to run Wasm as fast as possible on the JVM. The build-time compiler is designed to produce code that C2 can optimize deeply: millions of tiny functions, uniform opcode dispatch, comparison-heavy control flow at enormous scale. That's the whole point, and it's what makes the engine fast. It also means we exercise the JVM in ways that handwritten Java rarely does.</p>
<p>In the meantime, the workaround protects all users automatically on affected JDK versions. The upstream fix will ship in a future JDK release.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-learned">What we learned<a href="https://endive.run/blog/finding-a-jvm-jit-bug-the-hard-way#what-we-learned" class="hash-link" aria-label="Direct link to What we learned" title="Direct link to What we learned" translate="no">​</a></h2>
<p><strong>Correctness bugs are scarier than crashes.</strong> A wrong comparison result is silent, and downstream failures can look completely unrelated to the root cause. If the reporter hadn't been testing carefully, this could have gone unnoticed for much longer.</p>
<p><strong>Heisenbugs demand creative approaches.</strong> Every standard debugging technique (print statements, debuggers, test reduction, bisection) actively prevented this bug from manifesting. When observation collapses the phenomenon you're investigating, you need to find indirect evidence.</p>
<p><strong>Sometimes building new tooling beats reducing existing artifacts.</strong> I spent months trying to shrink a 213,000-line reproducer. In the end, building an entirely new compiler in 3 days produced a better result. The lesson isn't "always build new tools", it's "recognize when reduction has hit a wall and consider alternatives."</p>
<p><strong>Open source ecosystems work.</strong> A user reported the bug, we investigated and built workarounds, <a href="https://github.com/dmlloyd" target="_blank" rel="noopener noreferrer" class="">David Lloyd</a> helped file the upstream issue, and Roland Westrelin diagnosed the C2 root cause and wrote the fix. This chain from user report to compiler fix only works because the code is open and the communities are connected.</p>
<p>If you care about Java, WebAssembly, and the future of really portable software, <a href="https://github.com/bytecodealliance/endive" target="_blank" rel="noopener noreferrer" class="">come build with us</a>.</p>]]></content>
        <author>
            <name>Andrea Peruffo</name>
            <uri>https://github.com/andreaTP</uri>
        </author>
        <category label="Endive" term="Endive"/>
        <category label="Wasm" term="Wasm"/>
        <category label="Bug" term="Bug"/>
        <category label="JVM" term="JVM"/>
    </entry>
</feed>