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

Segfault in update_inherited_vtable: AppCDS, old bytecode, and redefineClasses

XMLWordPrintable

    • b24
    • x86_64
    • linux

      A DESCRIPTION OF THE PROBLEM :
      I have a test program that reproduces the issue. The Animal class declares an abstract method, and the Cat class extends Animal and implements the method. Both classes are compiled with bytecode version < 50 and dumped into a static AppCDS archive. The main method loads Animal without linking it, uses a Java agent to redefine an unrelated class, and loads Cat.

      Loading the Cat class segfaults the JVM with an error like:

      #
      # A fatal error has been detected by the Java Runtime Environment:
      #
      # SIGSEGV (0xb) at pc=0x00007f77c9901570, pid=53162, tid=53163
      #
      # JRE version: Java(TM) SE Runtime Environment (17.0.13+10) (build 17.0.13+10-LTS-268)
      # Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.13+10-LTS-268, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
      # Problematic frame:
      # V [libjvm.so+0xa3e570] klassVtable::update_inherited_vtable(Thread*, methodHandle const&, int, int, GrowableArray<InstanceKlass*>*)+0x2a0

      This problem isn't specific to Java agents though. For example, running new jdk.jfr.consumer.RecordingStream().start() in a daemon thread will redefine existing classes to add instrumentation, leading to the same error.

      I have a hypothesis for the root cause. In InstanceKlass::restore_unshareable_info, if JvmtiExport::has_redefined_a_class() is true, klassVtable::update_inherited_vtable is called to reinitialize the vtable. In this method, if the superclass vtable contains null pointers, then super_method->name() dereferences a null pointer.

      But why does the superclass vtable contain null pointers? Classes with old bytecode versions can't be linked at AppCDS dump time, so they are dumped in the loaded state and their vtables still contain null pointers. For these old classes, when has_redefined_a_class() is false, then the vtables are initialized at link time, and superclasses are always linked before their subclasses, so there's no problem. When has_redefined_a_class() is true, then the vtables are initialized at load time, under the assumption that the superclass vtable is already initialized. But this is a faulty assumption if the superclass isn't linked yet, and it was loaded when has_redefined_a_class() was still false.

      This issue can also be reproduced in openjdk/jdk17u, commit 1e20c7c. In InstanceKlass::restore_unshareable_info, if I replace this condition:

      if (JvmtiExport::has_redefined_a_class()) {

      with this one:

      if (JvmtiExport::has_redefined_a_class() && is_linked()) {

      then the JVM does not crash anymore. But I don't have a lot of context on this code, so I'm not sure if this change fixes the problem in all cases without breaking anything else.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Please see the source code section below.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The program executes normally.
      ACTUAL -
      The JVM crashes with the following error:
      #
      # A fatal error has been detected by the Java Runtime Environment:
      #
      # SIGSEGV (0xb) at pc=0x00007f77c9901570, pid=53162, tid=53163
      #
      # JRE version: Java(TM) SE Runtime Environment (17.0.13+10) (build 17.0.13+10-LTS-268)
      # Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.13+10-LTS-268, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
      # Problematic frame:
      # V [libjvm.so+0xa3e570] klassVtable::update_inherited_vtable(Thread*, methodHandle const&, int, int, GrowableArray<InstanceKlass*>*)+0x2a0

      ---------- BEGIN SOURCE ----------
      This Bash script contains all the source files and commands needed to reproduce the issue:

      #!/bin/bash

      set -eux -o pipefail

      cd "$(mktemp -d)"

      # Only used to get an older javac that can target old bytecode versions.
      curl -o jdk8.tar.gz https://cdn.azul.com/zulu/bin/zulu8.80.0.17-ca-jdk8.0.422-linux_x64.tar.gz
      tar -xf jdk8.tar.gz
      JAVA_HOME_8="zulu8.80.0.17-ca-jdk8.0.422-linux_x64"
      $JAVA_HOME_8/bin/java -version

      # Downloaded from this page:
      # https://www.oracle.com/java/technologies/downloads/#java17
      JAVA_HOME=$HOME/jdk-17.0.13
      $JAVA_HOME/bin/java -version

      cat > "Animal.java" << EOF
      public abstract class Animal {
        public abstract String sound();
      }
      EOF

      cat > "Cat.java" << EOF
      public class Cat extends Animal {
        @Override
        public String sound() {
          return "meow";
        }
      }
      EOF

      cat > "RedefineMe.java" << EOF
      public class RedefineMe {}
      EOF

      cat > "Agent.java" << EOF
      import java.lang.instrument.ClassDefinition;
      import java.lang.instrument.Instrumentation;

      public class Agent {
        private static Instrumentation inst;

        public static void premain(String agentArgs, Instrumentation inst) {
          Agent.inst = inst;
        }

        public static void redefineSomething() throws Exception {
          Class c = RedefineMe.class;
          byte[] bytes = c.getClassLoader().getResourceAsStream(c.getName().replace('.', '/') + ".class").readAllBytes();
          inst.redefineClasses(new ClassDefinition(c, bytes));
        }
      }
      EOF

      cat > "Main.java" << EOF
      public class Main {
        public static void main(String[] args) throws Exception {
          System.out.println("Main: loading Animal");
          Thread.currentThread().getContextClassLoader().loadClass("Animal");

          System.out.println("Main: redefining a class");
          Agent.redefineSomething();

          System.out.println("Main: loading Cat");
          Thread.currentThread().getContextClassLoader().loadClass("Cat");

          System.out.println("Main: using Cat");
          System.out.println("Cat says: " + new Cat().sound());
        }
      }
      EOF

      $JAVA_HOME_8/bin/javac -source 1.5 -target 1.5 Animal.java Cat.java

      $JAVA_HOME/bin/javac RedefineMe.java Agent.java Main.java

      cat > MANIFEST.MF << EOF
      Manifest-Version: 1.0
      Premain-Class: Agent
      Can-Redefine-Classes: true
      EOF

      $JAVA_HOME/bin/jar cfm main.jar MANIFEST.MF ./*.class

      echo "Cat" > classes.classlist

      $JAVA_HOME/bin/java \
        '-Xlog:cds*' \
        '-Xlog:class*' \
        -Xshare:dump \
        -XX:SharedArchiveFile=archive.jsa \
        -XX:SharedClassListFile=classes.classlist \
        -cp main.jar

      $JAVA_HOME/bin/java \
        '-Xlog:cds*' \
        '-Xlog:class*' \
        -XX:SharedArchiveFile=archive.jsa \
        -cp main.jar \
        -javaagent:main.jar \
        Main
      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      When JFR is enabled with a command-line option, like -XX:StartFlightRecording=duration=1s,filename=/dev/null, the JVM does not segfault anymore. JFR redefines some classes to add instrumentation, and the problem is avoided since has_redefined_a_class() is already true by the time Animal is loaded.

      FREQUENCY : always


            ccheung Calvin Cheung
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated:
              Resolved: