Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make internal classloader instrumentation indy compatible #12242

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ public class BootDelegationInstrumentation implements TypeInstrumentation {
public ElementMatcher<TypeDescription> typeMatcher() {
// just an optimization to exclude common class loaders that are known to delegate to the
// bootstrap loader (or happen to _be_ the bootstrap loader)
// The AgentClassLoader and InstrumentationModuleClassloaders are required to be excluded
// for the instrumentation to work properly. loadClass on those is invoked during Advice
// bootstrapping which therefore would cause an infinite recursion
return not(namedOneOf(
"java.lang.ClassLoader",
"com.ibm.oti.vm.BootstrapClassLoader",
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader"))
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader"))
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved
.and(extendsClass(named("java.lang.ClassLoader")));
}

Expand Down Expand Up @@ -136,13 +140,14 @@ public static Class<?> onEnter(@Advice.Argument(0) String name) {
return null;
}

@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(
@Advice.Return(readOnly = false) Class<?> result,
@Advice.Enter Class<?> resultFromBootstrapLoader) {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Return Class<?> result, @Advice.Enter Class<?> resultFromBootstrapLoader) {
if (resultFromBootstrapLoader != null) {
result = resultFromBootstrapLoader;
return resultFromBootstrapLoader;
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class ClassLoaderInstrumentationModule extends InstrumentationModule {
public class ClassLoaderInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public ClassLoaderInstrumentationModule() {
super("internal-class-loader");
}
Expand All @@ -26,13 +28,24 @@ public boolean defaultEnabled(ConfigProperties config) {
}

@Override
public boolean isIndyModule() {
return false;
public boolean isHelperClass(String className) {
// TODO: this can be removed when we drop inlined-advice support
// The advices can directly access this class in the AgentClassLoader with invokedynamic Advice
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
}

@Override
public boolean isHelperClass(String className) {
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
public boolean loadAdviceClassesEagerly() {
// This is required due to DefineClassInstrumentation
// Without this, we would get an infinite recursion on bootstrapping of that instrumentation:
// 1. ClassLoader.defineClass is invoked somewhere
// 2. The inserted invokedynamic for the instrumentation is reached
// 3. To bootstrap the advice, IndyBootstrap is invoked
// 4. IndyBootstrap tries to load the DefineClassInstrumentation Advice class
// 5. The loading calls ClassLoader.defineClass -> recursion, BOOM!
// We avoid this recursion by ensuring that the DefineClassInstrumentation Advice class
// is loaded eagerly before the corresponding invokedynamic is reached
return true;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,17 @@ public static DefineClassContext onEnter(
classLoader, className, classBytes, offset, length);
}

// TODO: the ToReturned does nothing except for signaling the AdviceTransformer that it must
// not touch this advice
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
// correctly
// we can therefore remove it, as soon as the AdviceTransformer is not applied anymore
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter DefineClassContext context) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
DefineClassHelper.afterDefineClass(context);
return returned;
}
}

Expand All @@ -68,9 +76,17 @@ public static DefineClassContext onEnter(
return DefineClassHelper.beforeDefineClass(classLoader, className, classBytes);
}

// TODO: the ToReturned does nothing except for signaling the AdviceTransformer that it must
// not touch this advice
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
// correctly
// we can therefore remove it, as soon as the AdviceTransformer is not applied anymore
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Enter DefineClassContext context) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
DefineClassHelper.afterDefineClass(context);
return returned;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
Expand All @@ -30,7 +31,15 @@ public class LoadInjectedClassInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("java.lang.ClassLoader"));
// We explicitly exclude our own classloaders:
// The invokedynamic bootstrapping involves calling loadClass on those, which would cause
// an infinite recursion
return extendsClass(named("java.lang.ClassLoader"))
JonasKunz marked this conversation as resolved.
Show resolved Hide resolved
.and(
not(
namedOneOf(
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader")));
}

@Override
Expand Down Expand Up @@ -65,11 +74,13 @@ public static Class<?> onEnter(
}

@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void onExit(
@Advice.Return(readOnly = false) Class<?> result, @Advice.Enter Class<?> loadedClass) {
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(
@Advice.Return Class<?> result, @Advice.Enter Class<?> loadedClass) {
if (loadedClass != null) {
result = loadedClass;
return loadedClass;
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

Expand All @@ -32,7 +34,12 @@ public class ResourceInjectionInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("java.lang.ClassLoader"));
return extendsClass(named("java.lang.ClassLoader"))
.and(
not(
namedOneOf(
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader")));
}

@Override
Expand Down Expand Up @@ -61,37 +68,37 @@ public void transform(TypeTransformer transformer) {
public static class GetResourceAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static URL onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) URL resource) {
if (resource != null) {
return;
}

URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
resource = helper;
@Advice.Return URL resource) {
if (resource == null) {
URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
return helper;
}
}
return resource;
}
}

@SuppressWarnings("unused")
public static class GetResourcesAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static Enumeration<URL> onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) Enumeration<URL> resources) {
@Advice.Return Enumeration<URL> resources) {
List<URL> helpers = HelperResources.loadAll(classLoader, name);
if (helpers.isEmpty()) {
return;
return resources;
}

if (!resources.hasMoreElements()) {
resources = Collections.enumeration(helpers);
return;
return Collections.enumeration(helpers);
}

List<URL> result = Collections.list(resources);
Expand All @@ -108,31 +115,30 @@ public static void onExit(
result.add(helperUrl);
}
}

resources = Collections.enumeration(result);
return Collections.enumeration(result);
}
}

@SuppressWarnings("unused")
public static class GetResourceAsStreamAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(
@Advice.AssignReturned.ToReturned
public static InputStream onExit(
@Advice.This ClassLoader classLoader,
@Advice.Argument(0) String name,
@Advice.Return(readOnly = false) InputStream inputStream) {
if (inputStream != null) {
return;
}

URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
try {
inputStream = helper.openStream();
} catch (IOException ignored) {
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
@Advice.Return InputStream inputStream) {
if (inputStream == null) {
URL helper = HelperResources.loadOne(classLoader, name);
if (helper != null) {
try {
return helper.openStream();
} catch (IOException ignored) {
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
}
}
}
return inputStream;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ private static int getJavaVersion() {
return Integer.parseInt(javaSpecVersion);
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// We explicitly override loadClass from ClassLoader to ensure
// that loadClass is properly excluded from our internal ClassLoader Instrumentations
// (e.g. LoadInjectedClassInstrumentation, BooDelegationInstrumentation)
// Otherwise this will cause recursion in invokedynamic linkage
return loadClass(name, false);
}

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// ContextStorageOverride is meant for library instrumentation we don't want it to apply to our
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,19 @@ default String getModuleGroup() {
default List<String> agentPackagesToHide() {
return Collections.emptyList();
}

/**
* By default, Advice classes are loaded lazily the first time the corresponding instrumented
* method is invoked. This function allows to change the behaviour so that all Advice classes are
* loaded and initialized as soon as a matching type is instrumented.
*
* <p>Note: this functionality currently does not work together with the AdviceTransformer.
* Therefore you should make your Advices indy-compatible (use @Advice.AssignReturned) before
* using this feature.
*
* @return true, if Advice classes should be loaded on instrumentation instead of first execution
*/
default boolean loadAdviceClassesEagerly() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ static byte[] transform(byte[] bytes) {
}));

TransformationContext context = new TransformationContext();
if (justDelegateAdvice) {
context.disableReturnTypeChange();
}
Comment on lines +70 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] without this the advice return type is assumed to be an array, which means skipOnIndex is added when using skipOn annotation attribute, and this breaks when advice is not inlined.

ClassVisitor cv =
new ClassVisitor(AsmApi.VERSION, cw) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ public class IndyBootstrap {

IndyBootstrapDispatcher.init(
MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType));

// Ensure that CallDepth is already loaded in case of bootstrapAdvice recursions with
// ClassLoader.loadClass
// This is required because CallDepth is a bootstrap class and therefore triggers our
// ClassLoader.loadClass instrumentations
Class.forName(CallDepth.class.getName());
Comment on lines +96 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] maybe add that this class needs to be explicitly initialized early because it's the one preventing recursive calls and is thus the one called first when loadClass advice method is executed. Adding a comment where this first call is made would also be relevant too just in case someone tries to add something before it.

Also, could we make this static init part of the advice static init instead ? If possible that would be easier to link with the advice method code. On the other hand this CallDepth init call might be needed elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in fact not the case that this class needs to be initialized early. It instead needs to be linked to IndyBootstrap early:
When some IndyBootstrap.bootstrapAdvice is executed for the first time, the JVM encounters a call to CallDepth.forClass. When linking this call, the JVM itself invokes AgentClassLoader.loadClass(CallDepth) to link that callsite. This in turn will delegate to the instrumented ClassLoader.loadClass because it is a bootstrap class. It doesn't matter where within bootstrapAdvice this call happens, it just happened by chance that it is the first one.

Also, could we make this static init part of the advice static init instead

For the reasons above: No, this is not possible, because it wouldn't link the usage of CallDepth from within the IndyBootstrap class.

} catch (Exception e) {
throw new IllegalStateException(e);
}
Expand Down
Loading
Loading