Skip to content
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 @@ -10,7 +10,7 @@

import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.java-conventions")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.classloader.stub;

import java.security.ProtectionDomain;

/**
* A placeholder for java.lang.ClassLoader to allow compilation of advice classes that invoke
* protected methods of ClassLoader (like defineClass and findLoadedClass). During the build we'll
* use shadow plugin to replace reference to this class with the real java.lang.ClassLoader.
*/
@SuppressWarnings("JavaLangClash")
public abstract class ClassLoader {
public abstract Class<?> findLoadedClass(String name);

public abstract Class<?> defineClass(
String name, byte[] b, int off, int len, ProtectionDomain protectionDomain);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ dependencies {
compileOnly("org.apache.commons:commons-lang3:3.12.0")
testImplementation("org.apache.commons:commons-lang3:3.12.0")

testInstrumentation(project(":instrumentation:internal:internal-class-loader:javaagent"))
testInstrumentation(project(":instrumentation:internal:internal-class-loader:javaagent", configuration = "shaded"))
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
id("otel.javaagent-instrumentation")
}

dependencies {
compileOnly(project(":javaagent-bootstrap"))
compileOnly(project(":javaagent-tooling"))
compileOnly(project(":instrumentation:internal:internal-class-loader:compile-stub"))

testImplementation(project(":javaagent-bootstrap"))

Expand All @@ -21,3 +24,32 @@ dependencies {
testImplementation("org.eclipse.platform:org.eclipse.osgi:3.13.200")
testImplementation("org.apache.felix:org.apache.felix.framework:6.0.2")
}

val shadedJar by tasks.registering(ShadowJar::class) {
from(zipTree(tasks.jar.get().archiveFile))
archiveClassifier.set("shaded")
}

tasks {
withType(ShadowJar::class) {
relocate("io.opentelemetry.javaagent.instrumentation.internal.classloader.stub", "java.lang")
}

assemble {
dependsOn(shadedJar)
}
}

// Create a consumable configuration for the shaded jar. We can't use the "shadow" configuration
// because that is taken by the agent-testing.jar
configurations {
consumable("shaded") {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("shaded"))
}
}
}

artifacts {
add("shaded", shadedJar)
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,37 @@ public void transform(TypeTransformer transformer) {
@SuppressWarnings("unused")
public static class LoadClassAdvice {

// Class loader stub is shaded back to the real class loader class. It is used to call protected
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried multiple approaches here (check the commits if interested). Firstly I tried creating a MethodHandles.Lookup in the advice and passing it along. This allowed getting access to MethodHandles for findLoadedClass and defineClass. Unfortunately it didn't work on jdk8, apparently ClassLoader is a restricted class there for which you can't create a Lookup for. Didn't like that a special case was needed for jdk8. Next I tried writing the advice with asm. Since we need to call multiple methods it was a sizable chunk of asm so instead I tried what you see here. We use a modified stub ClassLoader class that has findLoadedClass and defineClass as public so we could use them in the advice. During the build we shade the stub to java.lang.ClassLoader. The bytecode works because this advice is inlined into subclasses of ClassLoader that can call these protected methods. It isn't ideal either as it required some build script hacking to make the shading work.

// method from the advice that the compiler won't let us call directly. During runtime it is
// fine since this code is inlined into subclasses of ClassLoader that can call protected
// methods.
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
public static Class<?> onEnter(
@Advice.This ClassLoader classLoader, @Advice.Argument(0) String name) {
return InjectedClassHelper.loadHelperClass(classLoader, name);
@Advice.This java.lang.ClassLoader classLoader,
@Advice.This
io.opentelemetry.javaagent.instrumentation.internal.classloader.stub.ClassLoader
classLoaderStub,
@Advice.Argument(0) String name) {
InjectedClassHelper.HelperClassInfo helperClassInfo =
InjectedClassHelper.getHelperClassInfo(classLoader, name);
if (helperClassInfo != null) {
Class<?> clazz = classLoaderStub.findLoadedClass(name);
if (clazz != null) {
return clazz;
}
try {
byte[] bytes = helperClassInfo.getClassBytes();
return classLoaderStub.defineClass(
name, bytes, 0, bytes.length, helperClassInfo.getProtectionDomain());
} catch (LinkageError error) {
clazz = classLoaderStub.findLoadedClass(name);
if (clazz != null) {
return clazz;
}
throw error;
}
}
return null;
}

@AssignReturned.ToReturned
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import org.junit.jupiter.api.Test;

class LambdaInstrumentationTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import instrumentation.TestHelperClass;
import io.opentelemetry.javaagent.bootstrap.InstrumentationProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Field;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
package io.opentelemetry.javaagent.instrumentation.internal.reflection;

import io.opentelemetry.javaagent.bootstrap.InstrumentationProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.javaagent.bootstrap;

import java.security.ProtectionDomain;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
Expand Down Expand Up @@ -37,21 +38,27 @@ public static boolean isHelperClass(ClassLoader classLoader, String className) {
return helperClassDetector.test(classLoader, className);
}

private static volatile BiFunction<ClassLoader, String, Class<?>> helperClassLoader;
private static volatile BiFunction<ClassLoader, String, HelperClassInfo> helperClassInfo;

public static void internalSetHelperClassLoader(
BiFunction<ClassLoader, String, Class<?>> helperClassLoader) {
if (InjectedClassHelper.helperClassLoader != null) {
public static void internalSetHelperClassInfo(
BiFunction<ClassLoader, String, HelperClassInfo> helperClassInfo) {
if (InjectedClassHelper.helperClassInfo != null) {
// Only possible by misuse of this API, just ignore.
return;
}
InjectedClassHelper.helperClassLoader = helperClassLoader;
InjectedClassHelper.helperClassInfo = helperClassInfo;
}

public static Class<?> loadHelperClass(ClassLoader classLoader, String className) {
if (helperClassLoader == null) {
public static HelperClassInfo getHelperClassInfo(ClassLoader classLoader, String className) {
if (helperClassInfo == null) {
return null;
}
return helperClassLoader.apply(classLoader, className);
return helperClassInfo.apply(classLoader, className);
}

public interface HelperClassInfo {
byte[] getClassBytes();

ProtectionDomain getProtectionDomain();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

/** A marker interface implemented by virtual field accessor classes. */
public interface VirtualFieldAccessorMarker {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import java.lang.invoke.MethodHandles;
import java.util.Collection;

/** Helper class for detecting whether given class has virtual fields. */
Expand Down Expand Up @@ -48,4 +49,8 @@ public static boolean hasVirtualField(Class<?> clazz, String virtualFieldInterfa
public static void markVirtualFields(Class<?> clazz, Collection<String> virtualFieldClassName) {
classesWithVirtualFields.put(clazz, virtualFieldClassName);
}

public static MethodHandles.Lookup lookup() {
return MethodHandles.lookup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

/**
* A marker interface that signifies that virtual fields have been installed into the class that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ private static void installBytebuddyAgent(
// https://bugs.openjdk.org/browse/JDK-8164165
ThreadLocalRandom.current();

// AgentBuilder.Default constructor triggers sun.misc.Unsafe::objectFieldOffset called warning
// AgentBuilder$Default.<init>
// -> NexusAccessor.<clinit>
// -> ClassInjector$UsingReflection.<clinit>
// -> ClassInjector$UsingUnsafe.<clinit>
// -> WARNING: sun.misc.Unsafe::objectFieldOffset called by
// net.bytebuddy.dynamic.loading.ClassInjector$UsingUnsafe$Dispatcher$CreationAction
// we don't use byte-buddy nexus so we disable it and later restore the original value for the
// system property
String originalNexusDisabled = System.setProperty("net.bytebuddy.nexus.disabled", "true");
AgentBuilder agentBuilder =
new AgentBuilder.Default(
// default method graph compiler inspects the class hierarchy, we don't need it, so
Expand All @@ -147,6 +157,13 @@ private static void installBytebuddyAgent(
.with(AgentTooling.poolStrategy())
.with(AgentTooling.transformListener())
.with(AgentTooling.locationStrategy());
// restore the original value for the nexus disabled property
if (originalNexusDisabled != null) {
System.setProperty("net.bytebuddy.nexus.disabled", originalNexusDisabled);
} else {
System.clearProperty("net.bytebuddy.nexus.disabled");
}

if (JavaModule.isSupported()) {
agentBuilder = agentBuilder.with(new ExposeAgentBootstrapListener(inst));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealGetterName;
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealSetterName;

import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.tooling.muzzle.VirtualFieldMappings;
import java.util.HashMap;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package io.opentelemetry.javaagent.tooling.field;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.tooling.muzzle.ClassFileLocatorProvider;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.SyntheticState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldDetector;
import io.opentelemetry.javaagent.tooling.HelperInjector;
import io.opentelemetry.javaagent.tooling.TransformSafeLogger;
import io.opentelemetry.javaagent.tooling.instrumentation.InstrumentationModuleInstaller;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package io.opentelemetry.javaagent.tooling.field;

import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;

final class GeneratedVirtualFieldNames {

/**
Expand All @@ -13,7 +15,8 @@ final class GeneratedVirtualFieldNames {
* 'isolating' (or 'module') classloaders like jboss and osgi see injected classes. This works
* because we instrument those classloaders to load everything inside bootstrap packages.
*/
static final String DYNAMIC_CLASSES_PACKAGE = "io.opentelemetry.javaagent.bootstrap.field.";
static final String DYNAMIC_CLASSES_PACKAGE =
VirtualFieldAccessorMarker.class.getPackage().getName() + ".";

private GeneratedVirtualFieldNames() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealSetterName;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi;
import io.opentelemetry.javaagent.tooling.Utils;
import java.util.Arrays;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

package io.opentelemetry.javaagent.test


import groovy.transform.CompileStatic
import io.opentelemetry.javaagent.bootstrap.InjectedClassHelper
import io.opentelemetry.javaagent.tooling.AgentInstaller
import io.opentelemetry.javaagent.tooling.HelperInjector
import io.opentelemetry.javaagent.tooling.Utils
Expand All @@ -25,6 +26,37 @@ import static io.opentelemetry.instrumentation.test.utils.GcUtils.awaitGc

class HelperInjectionTest extends Specification {

@CompileStatic
class EmptyLoader extends URLClassLoader {
EmptyLoader() {
super(new URL[0], (ClassLoader) null)
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// the same code as added by LoadInjectedClassInstrumentation
InjectedClassHelper.HelperClassInfo helperClassInfo = InjectedClassHelper.getHelperClassInfo(this, name)
if (helperClassInfo != null) {
Class<?> clazz = findLoadedClass(name)
if (clazz != null) {
return clazz
}
try {
byte[] bytes = helperClassInfo.getClassBytes()
return defineClass(name, bytes, 0, bytes.length, helperClassInfo.getProtectionDomain())
} catch (LinkageError error) {
clazz = findLoadedClass(name)
if (clazz != null) {
return clazz
}
throw error
}
}

return super.loadClass(name, resolve)
}
}

def "helpers injected to non-delegating classloader"() {
setup:
URL[] helpersSourceUrls = new URL[1]
Expand All @@ -33,7 +65,7 @@ class HelperInjectionTest extends Specification {

String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass'
HelperInjector injector = new HelperInjector("test", [helperClassName], [], helpersSourceLoader, null)
AtomicReference<URLClassLoader> emptyLoader = new AtomicReference<>(new URLClassLoader(new URL[0], (ClassLoader) null))
AtomicReference<EmptyLoader> emptyLoader = new AtomicReference<>(new EmptyLoader())

when:
emptyLoader.get().loadClass(helperClassName)
Expand All @@ -42,7 +74,6 @@ class HelperInjectionTest extends Specification {

when:
injector.transform(null, null, emptyLoader.get(), null, null)
HelperInjector.loadHelperClass(emptyLoader.get(), helperClassName)
emptyLoader.get().loadClass(helperClassName)
then:
isClassLoaded(helperClassName, emptyLoader.get())
Expand Down
Loading