Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8358513

C2 Jit compilation fail accessing off-heap memory via Unsafe with ShenandoahGC

XMLWordPrintable

    • generic
    • generic

      ADDITIONAL SYSTEM INFORMATION :
      Reproduced on Linux Ubuntu 24.04

      Both with JDK17 and JDK21 (including JDK17 nightly build 2025-05-27).

      OpenJDK Runtime Environment (build 17.0.15+6-Ubuntu-0ubuntu124.04)
      OpenJDK Runtime Environment (build 21.0.7+6-Ubuntu-0ubuntu124.04)
      OpenJDK Runtime Environment (build 17.0.16-testing+0-builds.shipilev.net-openjdk-jdk17-dev-b818-20250527-1808)

      A DESCRIPTION OF THE PROBLEM :
      The C2 Jit compiler sometimes may reorder the Unsafe.getLong and Unsafe.putLong operations so that logic is broken. Problem is reproduced only with the ShenandoahGC and if method is C2 compiled.

      Other options work fine:
          1) jdk11 + any GC - OK
          2) jdk17/jdk21 + G1GC or ZGC - OK
          3) jdk17/jdk21 + ShenandoahGC + -XX:TieredStopAtLevel=3 - OK

      ***

      For example such Java method would return 0L even if there is an non-zero long value stored in the buffer before call:

          public long run(long addr) {
              long tmp = UNSAFE.getLong(addr);
              UNSAFE.putLong(addr, 0L);
              if (dummy != null)
                  System.err.println("never happen");
              return tmp;
          }

      Tests show that it is important that run() is method of the inner class (non-static). And dummy is field of the outer class.

      ***
      Reordering is obvious in the generated assembler code. Below are fragments obtained running the testcase attached.

      SenandoahGC. WRONG: write is reordered to be before the read:

          # {method} {0x00007f4738401510} 'run' '(J)J' in 'Test$TestRunner'
          # this: rsi:rsi = 'Test$TestRunner'
          # parm0: rdx:rdx = long
          .... skipped
          0x00007f47e0fb0b67: mov rbx, rdx
          0x00007f47e0fb0b6a: mov qword ptr [rbx], r12 <------------ write 0L to buffer
          0x00007f47e0fb0b6d: mov r8, r11
          0x00007f47e0fb0b70: shl r8, 3
          0x00007f47e0fb0b74: test byte ptr [r15 + 0x20], 1
          0x00007f47e0fb0b79: jne L0002
                       L0001: mov r11d, dword ptr [r8 + 0xc]
          0x00007f47e0fb0b7f: mov rbx, qword ptr [rbx] <------------ read form the buffer


      G1GC. CORRECT: read goes first before the write:

          # {method} {0x000072317b401510} 'run' '(J)J' in 'Test$TestRunner'
          # this: rsi:rsi = 'Test$TestRunner'
          # parm0: rdx:rdx = long
          .... skipped
          0x0000723214faa950: mov r11, rdx
          0x0000723214faa953: mov rax, qword ptr [r11] <------------ read from the buffer
          0x0000723214faa956: mov qword ptr [r11], r12 <------------ write 0L to buffer

      ***
      Something more or less similar was fixed in the https://bugs.openjdk.org/browse/JDK-8220714


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      javac Test.java
      java -XX:+UseShenandoahGC Test

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      All iterations are OK in the test:

      Test start
      0 iter passed
      1000000 iter passed
      2000000 iter passed
      3000000 iter passed
      4000000 iter passed
      5000000 iter passed
      6000000 iter passed
      7000000 iter passed
      8000000 iter passed
      9000000 iter passed
      ACTUAL -
      "RuntimeException: unexpected 0L in buffer" in some iteration (once the run() is C2 compiled)

      Test start
      0 iter passed
      1000000 iter passed
      2000000 iter passed
      3000000 iter passed
      4000000 iter passed
      5000000 iter passed
      Exception in thread "main" java.lang.RuntimeException: unexpected 0L in buffer
          at Test.test(Test.java:24)
          at Test.main(Test.java:16)

      ---------- BEGIN SOURCE ----------
      import java.lang.reflect.Field;
      import sun.misc.Unsafe;

      public class Test {
          private static final Unsafe UNSAFE = getUnsafe();

          private static final int BUFFERS_COUNT = 10_000_000;
          private static final long[] buffers = allocateBuffers(BUFFERS_COUNT);

          // Looks like everything would work OK if make this field static.
          private final Object dummy = null;

          private final TestRunner testRunner = new TestRunner();

          public static void main(String[] args) {
              new Test().test();
          }

          private void test() {
              System.err.println("Test start");

              for (int i = 0; i < BUFFERS_COUNT; i++) {
                  if (testRunner.run(buffers[i]) == 0)
                      throw new RuntimeException("unexpected 0L in buffer");

                  if (i % (BUFFERS_COUNT / 10) == 0)
                      System.err.println(i + " iter passed");
              }
          }

          // Everything would work OK if make TestRunner class static or
          // get rid of it and move run() method directly to Test class.
          public final class TestRunner {
              public long run(long addr) {
                  // Bug is here.
                  //
                  // getLong and putLong are reordered and tmp becomes 0L.
                  // But it can not be so since all buffers were filled with non-zero value after allocation.
                  long tmp = UNSAFE.getLong(addr);

                  // Everything would work OK if uncomment this check.
                  // if (tmp == 0)
                  // throw new RuntimeException("unexpected zero in buffer");

                  UNSAFE.putLong(addr, 0L);

                  // Everything would work OK if remove access and check for this dummy field.
                  if (dummy != null)
                      System.err.println("never happen");

                  return tmp;
              }
          }

          private static Unsafe getUnsafe() {
              try {
                  Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");

                  theUnsafe.setAccessible(true);

                  return (Unsafe) theUnsafe.get(null);
              }
              catch (NoSuchFieldException | IllegalAccessException e) {
                  throw new RuntimeException(e);
              }
          }

          private static long[] allocateBuffers(int count) {
              long[] buffers = new long[count];

              for (int i = 0; i < count; i++) {
                  buffers[i] = UNSAFE.allocateMemory(Long.BYTES);

                  // Put non-zero value to buffer.
                  UNSAFE.putLong(buffers[i], 333555777L);
              }

              return buffers;
          }
      }

      ---------- END SOURCE ----------

            roland Roland Westrelin
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: