Skip to content

OpenJ9 JIT throws a spurious ArrayIndexOutOfBoundsException from a redundant (Integer) unboxing cast path when the method is compiled on first invocation (-Xjit:count=0) #24910

Description

@TemporaryRepos

Java -version output

openjdk version "25.0.4.1" 2026-08-18 LTS
IBM Semeru Runtime Open Edition 25.0.4.15 (build 25.0.4.1+1-LTS)
Eclipse OpenJ9 VM 25.0.4.15 (build 25.0.4.1+1-openj9-0.61.0, JRE 25 Mac OS X aarch64-64-Bit 20260818_330 (JIT enabled, AOT enabled)
OpenJ9   - a1daf225f5
OMR      - ebd02d912
JCL      - cdc2304e39f based on jdk-25.0.4.1+1)

macOS 15, aarch64

Summary of problem

int var1 = (Integer) Integer.valueOf(Integer.MIN_VALUE); followed by if ((var1 * 2) < len) b[var1 * 2] = true; must never throw: Integer.MIN_VALUE * 2 wraps to 0 (JLS 15.17.2), so the guard 0 < len holds and the array index is 0. Under -Xjit:count=0 (compile on first invocation), the OpenJ9 JIT evaluates the boxed-cast path with a wrong value range — the bounds check is miscompiled for the very first iteration of the loop — and the program throws ArrayIndexOutOfBoundsException where none can legally occur.

The reproducer contains an in-program control path that performs the identical computation with a plain int literal (no cast) and runs it in the same JVM: the control path is correct in every configuration, which isolates the defect to the redundant unboxing-cast path (MATCH=diff). An L=7 deep-chain variant reproduces identically.

Affected version matrix (macOS 15, aarch64; identical repro):

Semeru build -Xjit:count=0 default -Xint
11.0.32 spurious AIOOBE on first iteration correct correct
17.0.20 spurious AIOOBE (5/5 reruns identical) correct correct
21.0.12 spurious AIOOBE correct correct
25.0.4 spurious AIOOBE correct correct

Controls: Temurin 17/21/25 in every mode tested (default, -Xcomp, -Xint) are correct; -Xjit:optlevel=cold / -Xjit:optlevel=hot on OpenJ9 are also correct — the defect requires the compile-on-first-invocation path (initial-compilation level code), not merely a low optimization level.

Violated specification: JLS 15.17.2 (integer multiplication wraps — Integer.MIN_VALUE * 2 == 0), and the general requirement that JIT-compiled code be observationally consistent with interpreted code of the same JVM. The interpreted execution (default, below the compilation threshold for a single invocation) runs the same method without error.

This issue was identified via differential testing across multiple JVM implementations (Temurin and IBM Semeru/OpenJ9, JDK 11–25).

Steps to reproduce

  1. javac repro.java
  2. java -Xjit:count=0 repro → CASTITERS=-1 CASTEX=java.lang.ArrayIndexOutOfBoundsException MATCH=diff (exception on the very first iteration; the no-cast control prints NOCASTITERS=99999 ... NOCASTEX=none)
  3. java repro → CASTITERS=99999 MATCH=same (correct)

Expected result

CASTITERS=99999
CASTHITS=100000
CASTEX=none
NOCASTITERS=99999
NOCASTHITS=100000
NOCASTEX=none
MATCH=same

on every conforming JVM in every execution mode (int overflow wraps per JLS 15.17.2, guard holds, index 0 valid).

Actual result

$ java -Xjit:count=0 repro
CHAINL=3
CASTITERS=-1
CASTHITS=0
CASTEX=java.lang.ArrayIndexOutOfBoundsException
NOCASTITERS=99999
NOCASTHITS=100000
NOCASTEX=none
MATCH=diff

Deterministic: byte-for-byte identical across 5/5 independent reruns (agent 3/3 plus main-session 2/2 on 17.0.20; single-run confirmation on 11.0.32 / 21.0.12 / 25.0.4).

Source code / Diagnostic files

// Explicit (Integer) cast unboxing of Integer.MIN_VALUE; loop guards array
// access with (var1 * 2) < len. MIN_VALUE*2 wraps to 0 so the guard must hold.
// An equivalent no-cast control path runs in the same program; both paths must
// agree (MATCH=same).
public class repro {
    static int len = 100;
    static int lastIters;
    static int lastHits;
    static boolean lastExc;

    static void runCast() {
        Integer boxed = Integer.valueOf(Integer.MIN_VALUE);
        int var1 = (Integer) boxed; // redundant cast + auto-unbox
        int iters = -1;
        int hits = 0;
        try {
            for (int i = 0; i < 100000; i++) {
                boolean[] b = new boolean[100];
                if ((var1 * 2) < len) { // MIN_VALUE*2 wraps to 0 -> 0 < 100
                    b[var1 * 2] = true;
                    hits++;
                }
                iters = i;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            lastExc = true;
        }
        lastIters = iters;
        lastHits = hits;
    }

    static void runNoCast() {
        int var1 = Integer.MIN_VALUE; // direct literal, no cast
        int iters = -1;
        int hits = 0;
        try {
            for (int i = 0; i < 100000; i++) {
                boolean[] b = new boolean[100];
                if ((var1 * 2) < len) {
                    b[var1 * 2] = true;
                    hits++;
                }
                iters = i;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            lastExc = true;
        }
        lastIters = iters;
        lastHits = hits;
    }

    static void realMain() {
        System.out.println("CHAINL=3");
        lastExc = false;
        runCast();
        int ci = lastIters;
        int ch = lastHits;
        boolean ce = lastExc;
        lastExc = false;
        runNoCast();
        int ni = lastIters;
        int nh = lastHits;
        boolean ne = lastExc;
        System.out.println("CASTITERS=" + ci);
        System.out.println("CASTHITS=" + ch);
        System.out.println("CASTEX=" + (ce ? "java.lang.ArrayIndexOutOfBoundsException" : "none"));
        System.out.println("NOCASTITERS=" + ni);
        System.out.println("NOCASTHITS=" + nh);
        System.out.println("NOCASTEX=" + (ne ? "java.lang.ArrayIndexOutOfBoundsException" : "none"));
        System.out.println("MATCH=" + ((ci == ni && ch == nh && ce == ne) ? "same" : "diff"));
    }

    public static void main(String[] args) {
        try {
            realMain();
        } catch (Throwable t) {
            System.err.println(t.getClass().getName() + ": " + t.getMessage());
            System.exit(1);
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions