1

I am trying to instrument dependencies using ASM 5.2. With the JDK 1.8 being mandated I am forced to use COMPUTE_FRAMES option in my class writer.

When using default ClassWriter it cannot find certain classes and throws the following exception

java.lang.RuntimeException: java.lang.ClassNotFoundException: XXX
    at org.objectweb.asm.ClassWriter.getCommonSuperClass(Unknown Source)
    at org.objectweb.asm.ClassWriter.a(Unknown Source)
    at org.objectweb.asm.Frame.a(Unknown Source)
    at org.objectweb.asm.Frame.a(Unknown Source)
    at org.objectweb.asm.MethodWriter.visitMaxs(Unknown Source)
    .....

In order to resolve this issue I override the getCommonSuperClass() method of the ClassWriter by creating a custom classWriter class and passing it the class loader from the transform() . I followed the advise from this thread: ASM 5.0.3 With Java 1.8 incorrect maxStack with Java.lang.VerifyError: Operand stack overflow

Here is the CustomClassWriter code:

class CustomClassWriter extends ClassWriter {


    ClassLoader classLoader;

    public CustomClassWriter(int writerFlag, ClassLoader loader)
    {
        super(writerFlag);
        this.classLoader = loader;
        System.out.println("Trace: Registering class loader from Code injector");
    }

    /**
     * This method returns common super class for both the classes. If no super class
     * is present it returns java/lang/Object class.
     * @param className1
     * @param className2
     * @return The String for the common super class of both the classes
     */
    protected String getCommonSuperClass(String className1, String className2)
    {
        Class class1;
        Class class2;
        //System.out.println("Trace: Default Class loader :" + getClass().getClassLoader().toString());
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println("Trace: Context class loader is " + contextClassLoader.toString());
        System.out.println("Trace: Loaded class loaded :" + classLoader.toString());

        try
        {
            class1 = Class.forName(className1.replace('/', '.'), false, this.classLoader);
            class2 = Class.forName(className2.replace('/', '.'), false, this.classLoader);
            System.out.println("Trace: Class 1" + class1.toString());
            System.out.println("Trace: Class 2" + class2.toString());
        }
        catch (Exception th) {
            throw new RuntimeException(th.getMessage());
        }

        if (class1.isAssignableFrom(class2)) {
            return className1;
        }
        if (class2.isAssignableFrom(class1)) {
            return className2;
        }

        if ((class1.isInterface()) || (class2.isInterface())) {
            return "java/lang/Object";
        }

        do {
            class1 = class1.getSuperclass();
        }
        while (!(class1.isAssignableFrom(class2)));
        return class1.getName().replace('.', '/');
    }


}

However, after doing this I run into weiered issue when I try to use the java agent with Eureka Netflix library :

Caused by: java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "org/apache/http/impl/client/AbstractHttpClient"
    at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_144]
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_144]
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_144]
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) ~[na:1.8.0_144]
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73) ~[na:1.8.0_144]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368) ~[na:1.8.0_144]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362) ~[na:1.8.0_144]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_144]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361) ~[na:1.8.0_144]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_144]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) ~[na:1.8.0_144]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_144]
    at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_144]
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_144]
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_144]
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) ~[na:1.8.0_144]
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73) ~[na:1.8.0_144]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:368) ~[na:1.8.0_144]
    at java.net.URLClassLoader$1.run(URLClassLoader.java:362) ~[na:1.8.0_144]
    at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_144]
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361) ~[na:1.8.0_144]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_144]
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) ~[na:1.8.0_144]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_144]
    at com.sun.jersey.client.apache4.ApacheHttpClient4.createDefaultClientHandler(ApacheHttpClient4.java:236) ~[jersey-apache-client4-1.19.1.jar:1.19.1]
    at com.sun.jersey.client.apache4.ApacheHttpClient4.create(ApacheHttpClient4.java:181) ~[jersey-apache-client4-1.19.1.jar:1.19.1]
    at com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl.<init>(EurekaJerseyClientImpl.java:52) ~[eureka-client-1.4.12.jar:1.4.12]
    ... 58 common frames omitted

I am not sure what is the exact issue here. How does class loader gets different gets duplicate definitions. What could be the way to resolve this? Any help would be greatly appreciated.

Edit: In addition to the above context, I am already repackaging the http client dependencies into custom namespace so that the agent doesn't instrument our own calls.

The problem precisely arises when I try to instrument Apache HTTP Client. Currently I have instrumentations for Apache HTTP Client 4.2 and 4.3 and when I boot the application by removing the instrumentation for 4.2 everything works fine.

Here are the definitions I am trying to instrument:

private final static String HTTP_CLIENT_43_CLASS_NAME = "org/apache/http/impl/client/InternalHttpClient";
    private final static String HTTP_CLIENT_METHOD_43_NAME = "doExecute";
    private final static String HTTP_CLIENT_METHOD_43_SIGNATURE = "(Lorg/apache/http/HttpHost;Lorg/apache/http/HttpRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/client/methods/CloseableHttpResponse;";

    private final static String HTTP_CLIENT_42_CLASS_NAME = "org/apache/http/impl/client/AbstractHttpClient";
    private final static String HTTP_CLIENT_METHOD_42_NAME = "execute";
    private final static String HTTP_CLIENT_METHOD_42_SIGNATURE = "(Lorg/apache/http/HttpHost;Lorg/apache/http/HttpRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/HttpResponse;";
Dhaval Doshi
  • 129
  • 6
  • 2
    You are not forced to use `COMPUTE_FRAMES`. The much better option is to consider what kind of Instrumentation you are doing and whether and in which way it affects the already existing frames. – Holger Nov 08 '17 at 07:41
  • @Holger can you please point me to some resources to do this? Earlier I was using COMPUTE_MAX with Skip frames and the code thew exceptions of StackMapFrame required when instrumenting oracle jdbc driver. My use case here is to calculate the start and stop time, record any exceptions and capture the sql string. There was one other stack overflow answer by you I went through on same but wasn't a lot helpful – Dhaval Doshi Nov 08 '17 at 19:19
  • 1
    First of all, you should understand the options. When when you use `SKIP_FRAMES`, the original frames are not processed, hence, will not show up in the instrumented code. This option can improve the performance when you use `COMPUTE_FRAMES`, as it will recalculate frames from scratch anyway. When you want to keep or adjust the original frames you *must not* use `SKIP_FRAMES`. Frames appear where branches merge. When you don’t modify the branches, but only insert or remove code between these frames, ASM will already adapt the locations. You only have to adapt them when you add variables, etc. – Holger Nov 09 '17 at 07:35

1 Answers1

4

The probelm is that you are loading classes during instrumentation by using ASM's common type resolver. This way, you are loading the class that you are currently instrumenting. Once your instrumentation completes, the JVM attempts to define the class but as it was previously defined, this is no longer possible.

You should avoid loading classes during instrumentation for this reason.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • This problem specifically arises when we try to instrument apache http client. As an update to the thread I am already repackaging the dependencies we have on apache http client to custom namespace so that agent doesn't try to instrument our own call. In the agent I instrument 2 versions of Apache HTTP Client (4.2 and 4.3) and when I remove the instrumentation of HTTP Client 4.2 everything works fine and the application boots up with Netflix eureka library too. I have updated the thread with the definitions I am instrumenting – Dhaval Doshi Nov 08 '17 at 19:23
  • This is probably because the library is showing a specific pattern in the instrumented codes stack frames that the other libraries do not expose. – Rafael Winterhalter Nov 08 '17 at 22:11
  • I see so do you have any suggestions how I can avoid using class.forName() to prevent duplicate loading of the class because I think the issue is stemming from this. Please let me know if you have suggestions. – Dhaval Doshi Nov 08 '17 at 22:35