Skip to content

Commit 37a54b5

Browse files
authored
Merge pull request #383 from DataDog/ark/muzzle-hookup
Enable Muzzle validation for Default Instrumentation Match phase
2 parents 47d6ef9 + 23d0439 commit 37a54b5

File tree

22 files changed

+383
-302
lines changed

22 files changed

+383
-302
lines changed

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ jobs:
187187
- dd-trace-java-version-scan
188188

189189
- run:
190-
name: Verify Version Scan
191-
command: ./gradlew verifyVersionScan --parallel --stacktrace --no-daemon --max-workers=6
190+
name: Verify Version Scan and Muzzle
191+
command: ./gradlew verifyVersionScan muzzle --parallel --stacktrace --no-daemon --max-workers=6
192192

193193
- save_cache:
194194
key: dd-trace-java-version-scan-{{ checksum "dd-trace-java.gradle" }}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class MuzzleExtension {
2+
String group
3+
String module
4+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import org.gradle.api.Plugin
2+
import org.gradle.api.Project
3+
4+
import java.lang.reflect.Method
5+
6+
/**
7+
* muzzle task plugin which runs muzzle validation against an instrumentation's compile-time dependencies.
8+
*
9+
* <p/>TODO: merge this with version scan
10+
*/
11+
class MuzzlePlugin implements Plugin<Project> {
12+
@Override
13+
void apply(Project project) {
14+
def bootstrapProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-bootstrap')
15+
def toolingProject = project.rootProject.getChildProjects().get('dd-java-agent').getChildProjects().get('agent-tooling')
16+
project.extensions.create("muzzle", MuzzleExtension)
17+
def muzzle = project.task('muzzle') {
18+
group = 'Muzzle'
19+
description = "Run instrumentation muzzle on compile time dependencies"
20+
doLast {
21+
List<URL> userUrls = new ArrayList<>()
22+
project.getLogger().info("Creating user classpath for: " + project.getName())
23+
for (File f : project.configurations.compileOnly.getFiles()) {
24+
project.getLogger().info( '--' + f)
25+
userUrls.add(f.toURI().toURL())
26+
}
27+
for (File f : bootstrapProject.sourceSets.main.runtimeClasspath.getFiles()) {
28+
project.getLogger().info( '--' + f)
29+
userUrls.add(f.toURI().toURL())
30+
}
31+
final ClassLoader userCL = new URLClassLoader(userUrls.toArray(new URL[0]), (ClassLoader) null)
32+
33+
project.getLogger().info("Creating dd classpath for: " + project.getName())
34+
Set<URL> ddUrls = new HashSet<>()
35+
for (File f : toolingProject.sourceSets.main.runtimeClasspath.getFiles()) {
36+
project.getLogger().info( '--' + f)
37+
ddUrls.add(f.toURI().toURL())
38+
}
39+
for(File f : project.sourceSets.main.runtimeClasspath.getFiles()) {
40+
project.getLogger().info( '--' + f)
41+
ddUrls.add(f.toURI().toURL())
42+
}
43+
44+
final ClassLoader agentCL = new URLClassLoader(ddUrls.toArray(new URL[0]), (ClassLoader) null)
45+
// find all instrumenters, get muzzle, and assert
46+
Method assertionMethod = agentCL.loadClass('datadog.trace.agent.tooling.muzzle.MuzzleVersionScanPlugin')
47+
.getMethod('assertInstrumentationNotMuzzled', ClassLoader.class)
48+
assertionMethod.invoke(null, userCL)
49+
}
50+
}
51+
// project.tasks.muzzle.dependsOn(bootstrapProject.tasks.shadowJar)
52+
project.tasks.muzzle.dependsOn(bootstrapProject.tasks.compileJava)
53+
project.tasks.muzzle.dependsOn(toolingProject.tasks.compileJava)
54+
project.afterEvaluate {
55+
project.tasks.muzzle.dependsOn(project.tasks.compileJava)
56+
}
57+
}
58+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
implementation-class=MuzzlePlugin

dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/muzzle/MuzzleBytecodeTransformTest.groovy

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import spock.lang.Specification
88

99
class MuzzleBytecodeTransformTest extends Specification {
1010

11-
/*
1211
def "muzzle fields added to all instrumentation"() {
1312
setup:
1413
List<Class> unMuzzledClasses = []
1514
List<Class> nonLazyFields = []
1615
List<Class> unInitFields = []
17-
for (final Object instrumenter : ServiceLoader.load(IntegrationTestUtils.getAgentClassLoader().loadClass("datadog.trace.agent.tooling.Instrumenter"), IntegrationTestUtils.getAgentClassLoader())) {
16+
for (Object instrumenter : ServiceLoader.load(IntegrationTestUtils.getAgentClassLoader().loadClass("datadog.trace.agent.tooling.Instrumenter"), IntegrationTestUtils.getAgentClassLoader())) {
17+
if (instrumenter.getClass().getName().endsWith("TraceConfigInstrumentation")) {
18+
// TraceConfigInstrumentation doesn't do muzzle checks
19+
// check on TracerClassInstrumentation instead
20+
instrumenter = IntegrationTestUtils.getAgentClassLoader().loadClass(instrumenter.getClass().getName() + '$TracerClassInstrumentation').newInstance()
21+
}
1822
Field f
1923
Method m
2024
try {
@@ -45,6 +49,5 @@ class MuzzleBytecodeTransformTest extends Specification {
4549
nonLazyFields == []
4650
unInitFields == []
4751
}
48-
*/
4952

5053
}

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import static net.bytebuddy.matcher.ElementMatchers.named;
99
import static net.bytebuddy.matcher.ElementMatchers.not;
1010

11-
import datadog.trace.agent.tooling.muzzle.Reference.Mismatch;
12-
import datadog.trace.agent.tooling.muzzle.ReferenceMatcher.MismatchException;
1311
import java.lang.instrument.Instrumentation;
1412
import java.util.ServiceLoader;
1513
import lombok.extern.slf4j.Slf4j;
@@ -91,19 +89,11 @@ public void onError(
9189
final JavaModule module,
9290
final boolean loaded,
9391
final Throwable throwable) {
94-
if (throwable instanceof MismatchException) {
95-
final MismatchException mismatchException = (MismatchException) throwable;
96-
log.debug("{}", mismatchException.getMessage());
97-
for (final Mismatch mismatch : mismatchException.getMismatches()) {
98-
log.debug("--{}", mismatch);
99-
}
100-
} else {
101-
log.debug(
102-
"Failed to handle {} for transformation on classloader {}: {}",
103-
typeName,
104-
classLoader,
105-
throwable.getMessage());
106-
}
92+
log.debug(
93+
"Failed to handle {} for transformation on classloader {}: {}",
94+
typeName,
95+
classLoader,
96+
throwable.getMessage());
10797
}
10898

10999
@Override

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
import static datadog.trace.agent.tooling.Utils.getConfigEnabled;
44
import static net.bytebuddy.matcher.ElementMatchers.any;
55

6+
import datadog.trace.agent.tooling.muzzle.Reference;
7+
import datadog.trace.agent.tooling.muzzle.ReferenceMatcher;
8+
import java.security.ProtectionDomain;
69
import java.util.Arrays;
710
import java.util.HashSet;
11+
import java.util.List;
812
import java.util.Map;
913
import java.util.Set;
1014
import lombok.extern.slf4j.Slf4j;
1115
import net.bytebuddy.agent.builder.AgentBuilder;
1216
import net.bytebuddy.description.type.TypeDescription;
1317
import net.bytebuddy.matcher.ElementMatcher;
18+
import net.bytebuddy.utility.JavaModule;
1419

1520
/**
1621
* Built-in bytebuddy-based instrumentation for the datadog javaagent.
@@ -42,11 +47,13 @@ public interface Instrumenter {
4247
@Slf4j
4348
abstract class Default implements Instrumenter {
4449
private final Set<String> instrumentationNames;
50+
private final String instrumentationPrimaryName;
4551
protected final boolean enabled;
4652

4753
public Default(final String instrumentationName, final String... additionalNames) {
4854
this.instrumentationNames = new HashSet<>(Arrays.asList(additionalNames));
4955
instrumentationNames.add(instrumentationName);
56+
instrumentationPrimaryName = instrumentationName;
5057

5158
// If default is enabled, we want to enable individually,
5259
// if default is disabled, we want to disable individually.
@@ -74,6 +81,35 @@ public AgentBuilder instrument(final AgentBuilder agentBuilder) {
7481
AgentBuilder.Identified.Extendable advice =
7582
agentBuilder
7683
.type(typeMatcher(), classLoaderMatcher())
84+
.and(
85+
new AgentBuilder.RawMatcher() {
86+
@Override
87+
public boolean matches(
88+
TypeDescription typeDescription,
89+
ClassLoader classLoader,
90+
JavaModule module,
91+
Class<?> classBeingRedefined,
92+
ProtectionDomain protectionDomain) {
93+
// Optimization: calling getInstrumentationMuzzle() inside this method prevents unnecessary loading of muzzle references during agentBuilder setup.
94+
final ReferenceMatcher muzzle = getInstrumentationMuzzle();
95+
if (null != muzzle) {
96+
List<Reference.Mismatch> mismatches =
97+
muzzle.getMismatchedReferenceSources(classLoader);
98+
if (mismatches.size() > 0) {
99+
log.debug(
100+
"Instrumentation muzzled: {} -- {} on {}",
101+
instrumentationPrimaryName,
102+
this.getClass().getName(),
103+
classLoader);
104+
}
105+
for (Reference.Mismatch mismatch : mismatches) {
106+
log.debug("-- {}", mismatch);
107+
}
108+
return mismatches.size() == 0;
109+
}
110+
return true;
111+
}
112+
})
77113
.transform(DDTransformers.defaultTransformers());
78114
final String[] helperClassNames = helperClassNames();
79115
if (helperClassNames.length > 0) {
@@ -85,6 +121,15 @@ public AgentBuilder instrument(final AgentBuilder agentBuilder) {
85121
return advice.asDecorator();
86122
}
87123

124+
/**
125+
* This method is implemented dynamically by compile-time bytecode transformations.
126+
*
127+
* <p>{@see datadog.trace.agent.tooling.muzzle.MuzzleGradlePlugin}
128+
*/
129+
protected ReferenceMatcher getInstrumentationMuzzle() {
130+
return null;
131+
}
132+
88133
@Override
89134
public String[] helperClassNames() {
90135
return new String[0];
@@ -105,11 +150,13 @@ protected boolean defaultEnabled() {
105150
return getConfigEnabled("dd.integrations.enabled", true);
106151
}
107152

108-
protected static String getPropOrEnv(final String name) {
153+
// TODO: move common config helpers to Utils
154+
155+
public static String getPropOrEnv(final String name) {
109156
return System.getProperty(name, System.getenv(propToEnvName(name)));
110157
}
111158

112-
private static String propToEnvName(final String name) {
159+
public static String propToEnvName(final String name) {
113160
return name.toUpperCase().replace(".", "_");
114161
}
115162
}

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGradlePlugin.java

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,32 @@
66
import net.bytebuddy.description.type.TypeDescription;
77
import net.bytebuddy.dynamic.DynamicType.Builder;
88

9+
/** Bytebuddy gradle plugin which creates muzzle-references at compile time. */
910
public class MuzzleGradlePlugin implements Plugin {
10-
// TODO:
11-
// - Optimizations
12-
// - Cache safe and unsafe classloaders
13-
// - Do reference generation at compile time
14-
// - lazy-load reference muzzle field
15-
// - Additional references to check
16-
// - Fields
17-
// - methods
18-
// - visit annotations
19-
// - visit parameter types
20-
// - visit method instructions
21-
// - method invoke type
22-
// - access flags (including implicit package-private)
23-
// - supertypes
24-
// - Misc
25-
// - Also match interfaces which extend Instrumenter
26-
// - Expose config instead of hardcoding datadog namespace (or reconfigure classpath)
27-
// - Run muzzle in matching phase (may require a rewrite of the instrumentation api)
28-
// - Documentation
29-
30-
private static final TypeDescription InstrumenterTypeDesc =
31-
new TypeDescription.ForLoadedType(Instrumenter.class);
11+
private static final TypeDescription DefaultInstrumenterTypeDesc =
12+
new TypeDescription.ForLoadedType(Instrumenter.Default.class);
3213

3314
@Override
3415
public boolean matches(final TypeDescription target) {
35-
// AutoService annotation is not retained at runtime. Check for instrumenter supertype
16+
// AutoService annotation is not retained at runtime. Check for Instrumenter.Default supertype
3617
boolean isInstrumenter = false;
37-
TypeDefinition instrumenter = target;
18+
TypeDefinition instrumenter = null == target ? null : target.getSuperClass();
3819
while (instrumenter != null) {
39-
if (instrumenter.getInterfaces().contains(InstrumenterTypeDesc)) {
20+
if (instrumenter.equals(DefaultInstrumenterTypeDesc)) {
4021
isInstrumenter = true;
4122
break;
4223
}
4324
instrumenter = instrumenter.getSuperClass();
4425
}
45-
// return isInstrumenter;
46-
return false;
26+
return isInstrumenter;
4727
}
4828

4929
@Override
5030
public Builder<?> apply(Builder<?> builder, TypeDescription typeDescription) {
5131
return builder.visit(new MuzzleVisitor());
5232
}
5333

34+
/** Compile-time Optimization used by gradle buildscripts. */
5435
public static class NoOp implements Plugin {
5536
@Override
5637
public boolean matches(final TypeDescription target) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package datadog.trace.agent.tooling.muzzle;
2+
3+
import datadog.trace.agent.tooling.HelperInjector;
4+
import datadog.trace.agent.tooling.Instrumenter;
5+
import java.lang.reflect.Method;
6+
import java.util.List;
7+
import java.util.ServiceLoader;
8+
9+
/**
10+
* Entry point for muzzle version scan gradle plugin.
11+
*
12+
* <p>For each instrumenter on the classpath, run muzzle validation and throw an exception if any
13+
* mismatches are detected.
14+
*
15+
* <p>Additionally, after a successful muzzle validation run each instrumenter's helper injector.
16+
*/
17+
public class MuzzleVersionScanPlugin {
18+
public static void assertInstrumentationNotMuzzled(ClassLoader cl) throws Exception {
19+
// muzzle validate all instrumenters
20+
for (Instrumenter instrumenter :
21+
ServiceLoader.load(Instrumenter.class, MuzzleGradlePlugin.class.getClassLoader())) {
22+
if (instrumenter.getClass().getName().endsWith("TraceConfigInstrumentation")) {
23+
// TraceConfigInstrumentation doesn't do muzzle checks
24+
// check on TracerClassInstrumentation instead
25+
instrumenter =
26+
(Instrumenter)
27+
MuzzleGradlePlugin.class
28+
.getClassLoader()
29+
.loadClass(instrumenter.getClass().getName() + "$TracerClassInstrumentation")
30+
.getDeclaredConstructor()
31+
.newInstance();
32+
}
33+
Method m = null;
34+
try {
35+
m = instrumenter.getClass().getDeclaredMethod("getInstrumentationMuzzle");
36+
m.setAccessible(true);
37+
ReferenceMatcher muzzle = (ReferenceMatcher) m.invoke(instrumenter);
38+
List<Reference.Mismatch> mismatches = muzzle.getMismatchedReferenceSources(cl);
39+
if (mismatches.size() > 0) {
40+
System.err.println(
41+
"FAILED MUZZLE VALIDATION: " + instrumenter.getClass().getName() + " mismatches:");
42+
for (Reference.Mismatch mismatch : mismatches) {
43+
System.err.println("-- " + mismatch);
44+
}
45+
throw new RuntimeException("Instrumentation failed Muzzle validation");
46+
}
47+
} finally {
48+
if (null != m) {
49+
m.setAccessible(false);
50+
}
51+
}
52+
}
53+
// run helper injector on all instrumenters
54+
for (Instrumenter instrumenter :
55+
ServiceLoader.load(Instrumenter.class, MuzzleGradlePlugin.class.getClassLoader())) {
56+
if (instrumenter.getClass().getName().endsWith("TraceConfigInstrumentation")) {
57+
// TraceConfigInstrumentation doesn't do muzzle checks
58+
// check on TracerClassInstrumentation instead
59+
instrumenter =
60+
(Instrumenter)
61+
MuzzleGradlePlugin.class
62+
.getClassLoader()
63+
.loadClass(instrumenter.getClass().getName() + "$TracerClassInstrumentation")
64+
.getDeclaredConstructor()
65+
.newInstance();
66+
}
67+
try {
68+
// Ratpack injects the scope manager as a helper.
69+
// This is likely a bug, but we'll grandfather it out of the helper checks for now.
70+
if (!instrumenter.getClass().getName().contains("Ratpack")) {
71+
// verify helper injector works
72+
final String[] helperClassNames = instrumenter.helperClassNames();
73+
if (helperClassNames.length > 0) {
74+
new HelperInjector(helperClassNames).transform(null, null, cl, null);
75+
}
76+
}
77+
} catch (Exception e) {
78+
System.err.println(
79+
"FAILED HELPER INJECTION. Are Helpers being injected in the correct order?");
80+
throw e;
81+
}
82+
}
83+
}
84+
85+
private MuzzleVersionScanPlugin() {}
86+
}

0 commit comments

Comments
 (0)