Skip to content
Draft
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 @@ -26,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.AsmVisitorWrapper;
Expand Down Expand Up @@ -130,18 +131,29 @@ private void prepareInstrumentation(InstrumenterModule module, int instrumentati

adviceShader = AdviceShader.with(module);

String[] helperClassNames = module.helperClassNames();
if (module.injectHelperDependencies()) {
helperClassNames = HelperScanner.withClassDependencies(helperClassNames);
}
// Resolve helper names lazily, and capture only GC-neutral values (not the module instance)
// so that the instrumenter can still be unloaded after setup, and load $Muzzle via the
// extended class loader like the muzzle check does.
final String instrumentationClass = module.getClass().getName();
final String[] declaredHelperClassNames = module.helperClassNames();
final boolean injectHelperDependencies = module.injectHelperDependencies();
helperTransformer =
helperClassNames.length > 0
? new HelperTransformer(
module.useAgentCodeSource(),
adviceShader,
module.getClass().getSimpleName(),
helperClassNames)
: null;
new HelperTransformer(
module.useAgentCodeSource(),
adviceShader,
module.getClass().getSimpleName(),
() -> {
String[] helperClassNames =
InstrumenterModule.loadStaticMuzzleHelperClassNames(
Utils.getExtendedClassLoader(), instrumentationClass);
if (null == helperClassNames) {
helperClassNames = declaredHelperClassNames;
}
if (injectHelperDependencies) {
helperClassNames = HelperScanner.withClassDependencies(helperClassNames);
}
return helperClassNames;
});

postProcessor = module.postProcessor();

Expand Down Expand Up @@ -385,7 +397,7 @@ static final class HelperTransformer extends HelperInjector implements AgentBuil
boolean useAgentCodeSource,
AdviceShader adviceShader,
String requestingName,
String... helperClassNames) {
Supplier<String[]> helperClassNames) {
super(useAgentCodeSource, adviceShader, requestingName, helperClassNames);
}
}
Expand Down
1 change: 1 addition & 0 deletions dd-java-agent/agent-tooling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {

testImplementation project(':dd-java-agent:testing')
testImplementation libs.bytebuddy
testImplementation libs.bundles.junit5
testImplementation group: 'com.google.guava', name: 'guava-testlib', version: '20.0'

jmhImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
Expand All @@ -42,7 +43,10 @@ public class HelperInjector implements Instrumenter.TransformingAdvice {
private final AdviceShader adviceShader;
private final String requestingName;

private final Set<String> helperClassNames;
// Helper names are resolved lazily: when a supplier is provided, resolution is deferred to the
// first transform() so that we don't load the generated $Muzzle class at agent install.
private final Supplier<String[]> helperClassNamesSupplier;
private volatile Set<String> helperClassNames;
private final Map<String, byte[]> dynamicTypeMap = new LinkedHashMap<>();

private final Map<ClassLoader, Boolean> injectedClassLoaders =
Expand Down Expand Up @@ -77,9 +81,27 @@ public HelperInjector(
this.requestingName = requestingName;
this.adviceShader = adviceShader;

this.helperClassNamesSupplier = null;
this.helperClassNames = new LinkedHashSet<>(asList(helperClassNames));
}

/**
* Construct HelperInjector whose helper names are resolved lazily on first {@link #transform}, to
* avoid resolving them (which may load the generated {@code $Muzzle} class) at agent install.
*/
public HelperInjector(
final boolean useAgentCodeSource,
final AdviceShader adviceShader,
final String requestingName,
final Supplier<String[]> helperClassNamesSupplier) {
this.useAgentCodeSource = useAgentCodeSource;
this.requestingName = requestingName;
this.adviceShader = adviceShader;

this.helperClassNamesSupplier = helperClassNamesSupplier;
this.helperClassNames = null;
}

public HelperInjector(
final boolean useAgentCodeSource,
final String requestingName,
Expand All @@ -88,11 +110,27 @@ public HelperInjector(
this.requestingName = requestingName;
this.adviceShader = null;

this.helperClassNamesSupplier = null;
helperClassNames = helperMap.keySet();
dynamicTypeMap.putAll(helperMap);
}

private Map<String, byte[]> getHelperMap() throws IOException {
/** Resolves helper class names, deferring to the supplier when one's provided. */
private Set<String> resolveHelperClassNames() {
Set<String> names = helperClassNames;
if (names == null) {
synchronized (this) {
names = helperClassNames;
if (names == null) {
names = new LinkedHashSet<>(asList(helperClassNamesSupplier.get()));
helperClassNames = names;
}
}
}
return names;
}

private Map<String, byte[]> getHelperMap(final Set<String> helperClassNames) throws IOException {
if (dynamicTypeMap.isEmpty()) {
final Map<String, byte[]> classnameToBytes = new LinkedHashMap<>();
for (String helperName : helperClassNames) {
Expand All @@ -117,6 +155,7 @@ public DynamicType.Builder<?> transform(
ClassLoader classLoader,
final JavaModule module,
final ProtectionDomain pd) {
final Set<String> helperClassNames = resolveHelperClassNames();
if (!helperClassNames.isEmpty()) {
if (classLoader == null) {
throw new UnsupportedOperationException(
Expand All @@ -135,7 +174,7 @@ public DynamicType.Builder<?> transform(
String.join(",", helperClassNames));
}

final Map<String, byte[]> classnameToBytes = getHelperMap();
final Map<String, byte[]> classnameToBytes = getHelperMap(helperClassNames);
final Collection<Class<?>> classes = injectClassLoader(classLoader, classnameToBytes);

// all datadog helper classes are in the unnamed module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
public final class HelperScanner extends ClassVisitor {
static final int READER_OPTIONS = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;

static final ClassFileLocator locator =
ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader());
final ClassFileLocator locator;

final MethodScanner methodScanner = new MethodScanner();

Expand All @@ -41,14 +40,30 @@ public final class HelperScanner extends ClassVisitor {
Set<String> uses;

HelperScanner() {
this(ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()));
}

HelperScanner(ClassFileLocator locator) {
super(Opcodes.ASM7, null);
this.locator = locator;
}

/** Expands helper class names to include any non-bootstrap classes they depend on. */
/**
* Expands helper class names with their non-bootstrap dependencies, via the agent class loader.
*/
public static String[] withClassDependencies(String... helperClassNames) {
return new HelperScanner().simulateClassLoading(helperClassNames);
}

/**
* Same as above, but reads bytecode via the given locator (e.g. during build time where the agent
* loader is absent).
*/
public static String[] withClassDependencies(
ClassFileLocator locator, String... helperClassNames) {
return new HelperScanner(locator).simulateClassLoading(helperClassNames);
}

/**
* Simulates class-loading by finding all classes required to load the helper classes as well as
* optional classes used in method instructions that may be needed later when invoking the method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,23 @@ public static ReferenceMatcher loadStaticMuzzleReferences(
}

/**
* @return Class names of helpers to inject into the user's classloader.
* <p><b>NOTE:</b> The order of the returned helper classes matters. If a muzzle check fails
* with a NoClassDefFoundError, as logged in build/reports/muzzle-*.txt, it is likely that one
* helper class depends on another that appears later in the list. In this case, the returned
* list must be reordered so that the referred helper class appears before the one that refers
* to it.
* @return the build-time inferred and manually-declared helper class names captured by {@code
* $Muzzle}, or {@code null} when none are available and fall back to {@link
* #helperClassNames()}.
*/
public static String[] loadStaticMuzzleHelperClassNames(
ClassLoader classLoader, String instrumentationClass) {
String muzzleClass = instrumentationClass + "$Muzzle";
try {
// helper class names captured at build-time; see MuzzleGenerator
return (String[])
classLoader.loadClass(muzzleClass).getMethod("helperClassNames").invoke(null);
} catch (Throwable e) {
return null;
}
}

/** Optional manual additions to the injected helper set. */
public String[] helperClassNames() {
return NO_HELPERS;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package datadog.trace.agent.tooling.muzzle;

import datadog.trace.bootstrap.Constants;
import java.util.function.Predicate;

/**
* Classifies a referenced class as an injectable tracer helper, a bootstrap class, or a library
* class — similar to OpenTelemetry's {@code HelperClassPredicate#isHelperClass}; however, the main
* signal here is whether the class was compiled from the instrumentation subproject's own output
* ({@code ownOutput}). {@link #HELPER_PREFIXES} additionally covers helpers that live in other
* tracer subprojects.
*/
public final class HelperClassPredicate {

static final String[] HELPER_PREFIXES = {
"datadog.trace.instrumentation.",
"datadog.opentelemetry.shim.",
"datadog.trace.agent.tooling.iast.",
"datadog.trace.agent.tooling.nativeimage.",
};

private final Predicate<String> ownOutput;

/**
* @param ownOutput whether a dotted class name was compiled from the instrumentation subproject's
* own output; injected because agent-tooling cannot resolve build directories itself.
*/
public HelperClassPredicate(final Predicate<String> ownOutput) {
this.ownOutput = ownOutput;
}

public boolean isHelperClass(final String className) {
return !isBootstrap(className) && (ownOutput.test(className) || matchesHelperPrefix(className));
}

private static boolean matchesHelperPrefix(final String className) {
for (final String prefix : HELPER_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
}
}
return false;
}

/**
* Whether the class is on the bootstrap class-path (or a JDK/SLF4J type) and so never injected.
*/
public static boolean isBootstrap(final String className) {
if (className.startsWith("java.")
|| className.startsWith("javax.")
|| className.startsWith("jdk.")
|| className.startsWith("com.sun.")
|| className.startsWith("sun.")
|| className.startsWith("org.slf4j.")
|| className.startsWith("datadog.slf4j.")) {
return true;
}
for (final String prefix : Constants.BOOTSTRAP_PACKAGE_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
}
}
return false;
}
}
Loading