6

I'm using a Java Agent and Javassist to add some logging to some JDK classes. Essentially when the system loads some TLS classes, Javassist will add some additional bytecode to them to help me debug some connection problems.

Here's the problem, given this class is included in the agent jar:

package com.something.myagent;
public class MyAgentPrinter {
    public static final void sayHello() {
        System.out.println("Hello!");
    }
}

In my agent's transform method, let's say I tried to invoke that class using javassist:

// this is only called for sun.security.ssl.Handshaker
ClassPool cp = getClassPool(classfileBuffer, className);
CtClass cc = cp.get(className);
CtMethod declaredMethod = cc.getDeclaredMethod("calculateKeys");
declaredMethod.insertAfter("com.something.myagent.MyAgentPrinter.sayHello();");
cc.freeze();
return cc.toBytecode();

You think that would work, but instead I get this:

java.lang.NoClassDefFoundError: com/something/myagent/MyAgentPrinter
    at sun.security.ssl.Handshaker.printLogLine(Handshaker.java)
    at sun.security.ssl.Handshaker.calculateKeys(Handshaker.java:1160)
    at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:292)

Is there any way to add that class [MyAgentPrinter] to the application's class path?

Pang
  • 9,564
  • 146
  • 81
  • 122
Jonathan S. Fisher
  • 8,189
  • 6
  • 46
  • 84

1 Answers1

8

Your Agent’s jar file is already added to the class path, as specified by the java.lang.instrument package documentation:

The agent class will be loaded by the system class loader (see ClassLoader.getSystemClassLoader). This is the class loader which typically loads the class containing the application main method. The premain methods will be run under the same security and classloader rules as the application main method.

This is the reason why Javassist can find the Agent’s classes when you are transforming the byte code.

The problem seems to be that you are transforming a sun.** class which is likely loaded by the bootstrap loader or extension loader. The standard class loading delegation is
application loader → extension loader → bootstrap loader, so classes available to the application loader are not available to classes loaded by the extension or bootstrap loader.

So, to make them available to all classes, you have to add the Agent’s classes to the bootstrap loader:

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) throws IOException {
        JarURLConnection connection = (JarURLConnection)
            MyAgent.class.getResource("MyAgent.class").openConnection();
        inst.appendToBootstrapClassLoaderSearch(connection.getJarFile());

        // proceed
    }
}

It’s critical to do this before any other action, i.e. before the classes you want to make available to instrumented code have been loaded. This implies that the Agent class itself, i.e. the class containing the premain method can not get accessed by the instrumented code. The Agent class also shouldn’t have direct references to MyAgentPrinter to avoid unintended early loading.

A more reliable way is to add a Boot-Class-Path entry to the Agent jar’s manifest, see the “Manifest Attributes” section of the package documentation, so that the entry gets added before the Agent starts. But then, the name of the jar file must not change afterwards.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • You say `The agent class will be loaded by the system class loader (see ClassLoader.getSystemClassLoader).` however the problem in the question is that the class `AgentPrinter` is not found. If `AgentPrinter` is in the same directory as `MyAgent` you re right otherwise it might be the case that I said – rakwaht Oct 20 '17 at 11:06
  • 1
    @rakwaht: my answer explains why Javassist does not fail, i.e. finds the class, while the *instrumented code* does fail. – Holger Oct 20 '17 at 11:09
  • It seems you were right. I deleted my answer since it was wrong. PS: I upvoted :) – rakwaht Oct 20 '17 at 11:22
  • Absolutely awesome answer. Thank you! – Jonathan S. Fisher Oct 20 '17 at 15:07
  • "The Agent class also shouldn’t have direct references to MyAgentPrinter to avoid unintended early loading." I got lost here. Could you please explain why is that? In my understanding, if the Agent class has reference to MyAgentPrinter, then the MyAgentPrinter will also be loaded by the bootstrap loader, so when the instrumented classes try to access MyAgentPrinter, they will find it is already loaded by the bootstrap loader and directly use it. So why do we want to avoid this? – Instein Jul 05 '22 at 05:46
  • 1
    @Instein Since the Agent class is not loaded by the bootstrap loader but the application loader, all referenced classes loaded before the jar file has been added to the bootstrap loader will be loaded by the application loader. The instrumented classes will find an identical class definition through the bootstrap loader, but it will be a different runtime class, which will cause problems in all scenarios where the Agent wants to exchange data with the instrumented classes. That’s why you have to defer the loading of these classes to a point after the jar has been added to the bootstrap loader. – Holger Jul 05 '22 at 07:02
  • "This implies that the Agent class itself, i.e. the class containing the premain method can not get accessed by the instrumented code."Could you also explain why? Say in javaagent we have Premain.java containing the premain method, and Premain.java use some function in Utils.java, do you mean that the instrumented classes should not access Premain.java and Utils.java at runtime? But I think it is ok because once the javaagent is added to the bootstrap loader before any other action, the instrumented classes will be able to access Premain.java and Utils.java which are loaded by bootstrap loader – Instein Oct 12 '22 at 19:43
  • 1
    @Instein the Premain class loaded through the bootstap loader is not the same class than the Agent’s Premain class loaded through the application class loader. These are two independent runtime classes, i.e. they have distinct static fields and their instances, if you create some, are incompatible. So why should the instrumented code interact with the Premain class at all, when it’s not interacting with the actual Agent (as it’s not the same class)? If the instrumented code and the Agent want to interact, they have to do this through runtime classes they both can see and agree on. – Holger Oct 13 '22 at 08:56
  • Thanks for your reply! I have two questions here. (1) If the javaagent is not added to the bootstrap loader, then there should only be one Premain class at runtime (application loader) and there should be no problem for the instrumented code interacting with this Premain class, right? (2) If the javaagent is added to the bootstrap loader, why there will be two Premain classes? "Prior to locating and loading a class, a class loader checks whether the class’s parent can load—or already has loaded—the required class." So the application class loader will find Premain classes already loaded? – Instein Oct 13 '22 at 20:15
  • 1
    @Instein the whole point of adding the classes to the bootstrap loader is that instrumented classes not loaded through the application loader do not see classes loaded by the application loader (like the Agent’s classes). In the OP’s case, it’s the class `sun.security.ssl.Handshaker` that had been instrumented. If you only instrument classes loaded through the application loader or its sub loaders, you don’t have a problem (and don’t need to add the classes to the bootstrap loader). – Holger Oct 14 '22 at 09:18