-
Bug
-
Resolution: Unresolved
-
P3
-
9, 10, 11
-
x86_64
-
generic
ADDITIONAL SYSTEM INFORMATION :
All systems and OS, same issue with JDK 9 and 10
A DESCRIPTION OF THE PROBLEM :
JVM invokes `jdk.internal.module.Modules.transformedByAgent` when a given module (a class from the module) has been transformed by the agent such that the module can call into code provided by the boot class path or application class path. Unfortunately if the agent attempts to modify a class by adding additional interfaces (from the agent, hence unnamed module), then it triggers `IllegalAccessError` as below:
It appears that the superinterface "verification" is executed before `jdk.internal.module.Modules.transformedByAgent` is invoked.
Such an assumption is supported by the fact that if there's another other class that trigger transformation on the same module without extra interface (hence successfully trigger `transformedByAgent` on that module) , then all subsequent transformations on the same module would work, even with reference to interface from unnamed module from the agent.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create an agent that transforms a class from module java.base, for example `java.net.HttpURLConnection`
2. Use bytecode library to modify the class to implements an extra interface that is provided by the agent
3. Package the agent
4. Create a test driver that references `HttpURLConnection`
5. Starts the test driver with javaagent argument pointing to the packaged agent
6. `IllegalAccessError` is triggered
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
JVM should be able to load the `HttpURLConnection` with the interface provided by the agent
ACTUAL -
Exception as below is thrown
Exception in thread "main" java.lang.IllegalAccessError: superinterface check failed: class java.net.HttpURLConnection (in module java.base) cannot access class agent.MyInterface (in unnamed module @0x77b52d12) because module java.base does not read unnamed module @0x77b52d12
at java.base/sun.net.www.protocol.http.Handler.openConnection(Handler.java:62)
at java.base/sun.net.www.protocol.http.Handler.openConnection(Handler.java:57)
at java.base/java.net.URL.openConnection(URL.java:1051)
at TestDriver.main(TestDriver.java:16)
---------- BEGIN SOURCE ----------
==================================
Agent.java
==================================
package agent;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
public class Agent {
public static void premain(String args, Instrumentation instrumentation){
instrumentation.addTransformer(new ClassFileTransformer() {
private ClassPool classPool;
{
classPool = new ClassPool();
if (ClassLoader.getSystemClassLoader() != null) {
//system class loader should have access to bootstrap class loader, which is where we make our agent classes available via MANIFEST.MF's Boot-Class-Path
classPool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
}
}
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> inputClass, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
if (className.equals("java/util/concurrent/ThreadPoolExecutor")) { //doesn't have to be ThreadPoolExecutor, can be any class from module java.base, if this is triggered before `HttpURLConnection` transformation, then no longer trigger `IllegalAccessError`
System.out.println("Touching ThreadPoolExecutor");
return classBytes;//do nothing, but return non-null will convince JVM that it's been touched
}
if (className.equals("java/net/HttpURLConnection")) {
try {
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classBytes));
ctClass.addInterface(classPool.get(MyInterface.class.getName())); //tag an interface to this class
System.out.println("Tagging interface to HttpURLConnection");
return ctClass.toBytecode();
} catch (NotFoundException | IOException | CannotCompileException e) {
e.printStackTrace();
return null;
}
}
return null;
}
});
}
public interface MyInterface {}
}
==================================
agent's MANIFEST.MF
==================================
Manifest-Version: 1.0
Premain-Class: agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Boot-Class-Path: agent-module-issue-0.0.1-SNAPSHOT-jar-with-dependencies.jar
Can-Set-Native-Method-Prefix: true
==================================
agent's pom.xml
==================================
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>agent-module-issue</groupId>
<artifactId>agent-module-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestFile>${basedir}/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<!-- Maven Assembly Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- get all project dependencies -->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>${basedir}/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>
</project>
==================================
TestDriver.java
==================================
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ThreadPoolExecutor;
public class TestDriver {
public static void main(String[] args) throws InterruptedException, ClassNotFoundException, Exception, IOException {
// Class<?> dummy = ThreadPoolExecutor.class; //if this is uncommented, then exception is no longer thrown
new URL("http://www.google.com").openConnection().connect();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
If the above TestDriver.java code has the `Class<?> dummy = ThreadPoolExecutor.class;` uncommented, then the classloading would work fine for `HttpURLConnection`
FREQUENCY : always
All systems and OS, same issue with JDK 9 and 10
A DESCRIPTION OF THE PROBLEM :
JVM invokes `jdk.internal.module.Modules.transformedByAgent` when a given module (a class from the module) has been transformed by the agent such that the module can call into code provided by the boot class path or application class path. Unfortunately if the agent attempts to modify a class by adding additional interfaces (from the agent, hence unnamed module), then it triggers `IllegalAccessError` as below:
It appears that the superinterface "verification" is executed before `jdk.internal.module.Modules.transformedByAgent` is invoked.
Such an assumption is supported by the fact that if there's another other class that trigger transformation on the same module without extra interface (hence successfully trigger `transformedByAgent` on that module) , then all subsequent transformations on the same module would work, even with reference to interface from unnamed module from the agent.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create an agent that transforms a class from module java.base, for example `java.net.HttpURLConnection`
2. Use bytecode library to modify the class to implements an extra interface that is provided by the agent
3. Package the agent
4. Create a test driver that references `HttpURLConnection`
5. Starts the test driver with javaagent argument pointing to the packaged agent
6. `IllegalAccessError` is triggered
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
JVM should be able to load the `HttpURLConnection` with the interface provided by the agent
ACTUAL -
Exception as below is thrown
Exception in thread "main" java.lang.IllegalAccessError: superinterface check failed: class java.net.HttpURLConnection (in module java.base) cannot access class agent.MyInterface (in unnamed module @0x77b52d12) because module java.base does not read unnamed module @0x77b52d12
at java.base/sun.net.www.protocol.http.Handler.openConnection(Handler.java:62)
at java.base/sun.net.www.protocol.http.Handler.openConnection(Handler.java:57)
at java.base/java.net.URL.openConnection(URL.java:1051)
at TestDriver.main(TestDriver.java:16)
---------- BEGIN SOURCE ----------
==================================
Agent.java
==================================
package agent;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
public class Agent {
public static void premain(String args, Instrumentation instrumentation){
instrumentation.addTransformer(new ClassFileTransformer() {
private ClassPool classPool;
{
classPool = new ClassPool();
if (ClassLoader.getSystemClassLoader() != null) {
//system class loader should have access to bootstrap class loader, which is where we make our agent classes available via MANIFEST.MF's Boot-Class-Path
classPool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
}
}
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> inputClass, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
if (className.equals("java/util/concurrent/ThreadPoolExecutor")) { //doesn't have to be ThreadPoolExecutor, can be any class from module java.base, if this is triggered before `HttpURLConnection` transformation, then no longer trigger `IllegalAccessError`
System.out.println("Touching ThreadPoolExecutor");
return classBytes;//do nothing, but return non-null will convince JVM that it's been touched
}
if (className.equals("java/net/HttpURLConnection")) {
try {
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classBytes));
ctClass.addInterface(classPool.get(MyInterface.class.getName())); //tag an interface to this class
System.out.println("Tagging interface to HttpURLConnection");
return ctClass.toBytecode();
} catch (NotFoundException | IOException | CannotCompileException e) {
e.printStackTrace();
return null;
}
}
return null;
}
});
}
public interface MyInterface {}
}
==================================
agent's MANIFEST.MF
==================================
Manifest-Version: 1.0
Premain-Class: agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Boot-Class-Path: agent-module-issue-0.0.1-SNAPSHOT-jar-with-dependencies.jar
Can-Set-Native-Method-Prefix: true
==================================
agent's pom.xml
==================================
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>agent-module-issue</groupId>
<artifactId>agent-module-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifestFile>${basedir}/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<!-- Maven Assembly Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- get all project dependencies -->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>${basedir}/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>
</project>
==================================
TestDriver.java
==================================
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ThreadPoolExecutor;
public class TestDriver {
public static void main(String[] args) throws InterruptedException, ClassNotFoundException, Exception, IOException {
// Class<?> dummy = ThreadPoolExecutor.class; //if this is uncommented, then exception is no longer thrown
new URL("http://www.google.com").openConnection().connect();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
If the above TestDriver.java code has the `Class<?> dummy = ThreadPoolExecutor.class;` uncommented, then the classloading would work fine for `HttpURLConnection`
FREQUENCY : always