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
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
- relates to
-
JDK-8343889 Test runtime/cds/appcds/redefineClass/RedefineBasicTest.java failed
- Resolved
-
JDK-8344046 Tests under cds/appcds/jvmti/redefineClasses should have @requires vm.cds
- Resolved
- links to
-
Commit(master) openjdk/jdk/ccda8159
-
Review(master) openjdk/jdk/21667