zhihan / janala2-gradle

CATG updated

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

LinkageError when referencing own <init> in try-catch block

rohanpadhye opened this issue · comments

I came across a subtle bug when trying to instrument a Java class that referenced its own constructor in a try-catch block. Here's a minimal test case Hello.java to reproduce the bug:

public class Hello {
    public static void main(String[] args) {
      try {
        Object o = new Hello();
      } catch (Exception e) {}
    }
}

If we try to instrument this class and log instructions as follows:

$JANALA_DIR/scripts/instrument.sh Hello

We get the following LinkageError with Java 1.8.0_102:

Exception in thread "main" java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "Hello"
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

The funny thing is that if we replace new Hello() with new Object() or any new Foo() the instrumentation works just fine. Similarly, the error disappears if we remove the surrounding try-catch block.

However, this is not a bug inherent to Janala. The problem is actually rooted in ASM, because the same error occurs even if you strip off all instrumentation and simply read/write the exact same bytecode in janala.instrument.SnoopInstructionTransformer (by replacing the ClassVisitor with a visitor that does nothing). In such a case, disabling COMPUTE_FRAMES in the constructor of ASM's ClassWriter gets rid of the LinkageError, indicating that the bug lies within the computation of stack map frames in the bytecode.

After a lot of digging, I found that the justification of this error is documented in the ASM JavaDoc for ClassVisitor#getCommonSuperClass, which is internally called by the code that computes stack map frames when COMPUTE_FRAMES is provided in the constructor of ClassVisitor.

The JavaDoc says the following:

Returns the common super type of the two given types. The default implementation of this method loads the two given classes and uses the java.lang.Class methods to find the common super class. It can be overridden to compute this common super type in other ways, in particular without actually loading any class, or to take into account the class that is currently being generated by this ClassWriter, which can of course not be loaded since it is under construction.

The LinkageError described above occurs because of the computation of common supertypes of Hello and Object, which loads Hello using the system class loader just before Hello is instrumented. Therefore, this bug is specific to applications which perform on-the-fly instrumentation (such as Janala), but would not affect ahead-of-time instrumentation using ASM.

The only fix is to override the getCommonSuperClass method to return an approximation of the common super class without using the system class loader. I found an example of this technique in ASM's own test suite.

I can send a pull request if the issue is confirmed.