-
Notifications
You must be signed in to change notification settings - Fork 355
Instrument LambdaMetafactory and preserve Runnable lambda identity during context propagation #12346
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
base: master
Are you sure you want to change the base?
Instrument LambdaMetafactory and preserve Runnable lambda identity during context propagation #12346
Changes from all commits
ec68f8e
5318053
938c2a5
0f6dfee
e34f5fc
b443c58
52137c8
3bbe48b
7af9fc4
81421d4
c2c7192
11d0748
885ffac
422b951
6f9490f
5742c20
5655a6f
a721063
1bc6bbc
b11c8c6
35be726
1e0c3ce
0c05100
9c89b93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package datadog.trace.bootstrap.instrumentation.java.lang.invoke; | ||
|
|
||
| /** Transforms a generated lambda class before it is defined. */ | ||
| public interface LambdaTransformer { | ||
| /** | ||
| * @param slashClassName internal (slash-separated) name of the generated lambda class | ||
| * @param targetClass the class declaring the lambda | ||
| * @param classBytes the freshly generated lambda class bytes | ||
| * @param interfaceClassName the functional interface implemented by the lambda | ||
| * @return the transformed bytes, or {@code null}/the original bytes if unchanged | ||
| */ | ||
| byte[] transform( | ||
| String slashClassName, Class<?> targetClass, byte[] classBytes, String interfaceClassName); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package datadog.trace.bootstrap.instrumentation.java.lang.invoke; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** Transforms eligible lambda bytes before definition, falling back to the original on failure. */ | ||
| public final class LambdaTransformerHelper { | ||
| private static final Logger log = LoggerFactory.getLogger(LambdaTransformerHelper.class); | ||
|
|
||
| // Agent transformation may itself create lambdas. | ||
| private static final ThreadLocal<Boolean> TRANSFORMING = new ThreadLocal<>(); | ||
|
|
||
| private LambdaTransformerHelper() {} | ||
|
|
||
| /** | ||
| * @param classBytes the generated lambda class bytes | ||
| * @param lambdaClassName internal (slash-separated) name of the generated lambda class | ||
| * @param targetClass the class declaring the lambda | ||
| * @param interfaceClass the functional interface implemented by the lambda | ||
| * @return possibly transformed bytes; the original bytes on any failure | ||
| */ | ||
| public static byte[] transform( | ||
| byte[] classBytes, String lambdaClassName, Class<?> targetClass, Class<?> interfaceClass) { | ||
| try { | ||
| if (interfaceClass == null) { | ||
| return classBytes; | ||
| } | ||
| String interfaceName = interfaceClass.getName(); | ||
| LambdaTransformer transformer = LambdaTransformerHolder.get(); | ||
| if (transformer == null) { | ||
| log.debug("Lambda {} skipped: no transformer registered", lambdaClassName); | ||
| return classBytes; | ||
| } | ||
| if (targetClass == null) { | ||
| log.debug("Lambda {} skipped: no target class", lambdaClassName); | ||
| return classBytes; | ||
| } | ||
| // Skip lambdas declared by the agent itself to avoid self-instrumentation and recursion. | ||
| String targetName = targetClass.getName(); | ||
| if (targetName.startsWith("datadog.") || targetName.startsWith("net.bytebuddy.")) { | ||
| log.debug("Lambda {} skipped: declared by the agent", lambdaClassName); | ||
| return classBytes; | ||
| } | ||
| if (Boolean.TRUE.equals(TRANSFORMING.get())) { | ||
| log.debug("Lambda {} skipped: re-entrant transform", lambdaClassName); | ||
| return classBytes; | ||
| } | ||
| TRANSFORMING.set(Boolean.TRUE); | ||
| try { | ||
| byte[] result = | ||
| transformer.transform(lambdaClassName, targetClass, classBytes, interfaceName); | ||
| if (result == null) { | ||
| log.debug("Lambda {} not transformed", lambdaClassName); | ||
| return classBytes; | ||
| } | ||
| return result; | ||
| } finally { | ||
| TRANSFORMING.remove(); | ||
| } | ||
| } catch (Throwable e) { | ||
| log.debug("Lambda {} skipped: {}", lambdaClassName, e.toString()); | ||
| return classBytes; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package datadog.trace.bootstrap.instrumentation.java.lang.invoke; | ||
|
|
||
| /** | ||
| * Holds the {@link LambdaTransformer} registered by the agent installer. Lives on the bootstrap | ||
| * class path so it is reachable from instrumented {@code java.lang.invoke} code. | ||
| */ | ||
| public final class LambdaTransformerHolder { | ||
| private static volatile LambdaTransformer transformer; | ||
|
|
||
| private LambdaTransformerHolder() {} | ||
|
|
||
| public static void set(LambdaTransformer transformer) { | ||
| LambdaTransformerHolder.transformer = transformer; | ||
| } | ||
|
|
||
| public static LambdaTransformer get() { | ||
| return transformer; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Reserved lambda interface manifest (inactive) | ||
|
|
||
| # Production lambda transformation is intentionally not enabled for any functional interface. | ||
| # This file is not read at build time or runtime. Runtime selection is driven exclusively by | ||
| # enabled Instrumenter.ForLambda implementations and also requires trace.lambda.enabled. | ||
|
|
||
| # If a validated manifest is introduced later, entries will use the ClassNameTrie format: | ||
| # | ||
| # 1 java.lang.Runnable | ||
|
|
||
| # Tests register Runnable from TestRunnableLambdaInstrumentation; this file has no effect on them. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import static datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnoresMatcher.globalIgnoresMatcher; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; | ||
|
|
||
| import datadog.environment.JavaVirtualMachine; | ||
| import datadog.environment.SystemProperties; | ||
| import datadog.trace.agent.tooling.bytebuddy.SharedTypePools; | ||
| import datadog.trace.agent.tooling.bytebuddy.iast.TaintableRedefinitionStrategyListener; | ||
|
|
@@ -19,6 +20,9 @@ | |
| import datadog.trace.api.telemetry.IntegrationsCollector; | ||
| import datadog.trace.bootstrap.FieldBackedContextAccessor; | ||
| import datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter; | ||
| import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformer; | ||
| import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHelper; | ||
| import datadog.trace.bootstrap.instrumentation.java.lang.invoke.LambdaTransformerHolder; | ||
| import datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper; | ||
| import datadog.trace.util.AgentTaskScheduler; | ||
| import de.thetaphi.forbiddenapis.SuppressForbidden; | ||
|
|
@@ -35,6 +39,7 @@ | |
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.function.BooleanSupplier; | ||
| import java.util.function.Function; | ||
| import net.bytebuddy.ByteBuddy; | ||
| import net.bytebuddy.agent.builder.AgentBuilder; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
|
|
@@ -163,6 +168,15 @@ public static ClassFileTransformer installBytebuddyAgent( | |
| // .with(AgentBuilder.LambdaInstrumentationStrategy.ENABLED) | ||
| .ignore(globalIgnoresMatcher(skipAdditionalLibraryMatcher)); | ||
|
|
||
| boolean lambdaTransformationEnabled = | ||
| !Platform.isNativeImageBuilder() | ||
| && InstrumenterConfig.get() | ||
| .isIntegrationEnabled(Collections.singleton("lambda"), false); | ||
| if (lambdaTransformationEnabled) { | ||
| // The injected metafactory call needs java.base to read the bootstrap helper's module. | ||
| agentBuilder = agentBuilder.assureReadEdgeTo(inst, LambdaTransformerHelper.class); | ||
| } | ||
|
|
||
| if (DEBUG) { | ||
| agentBuilder = | ||
| agentBuilder | ||
|
|
@@ -253,12 +267,86 @@ public void applied(Iterable<String> instrumentationNames) { | |
|
|
||
| InstrumenterState.resetDefaultState(); | ||
| try { | ||
| return transformerBuilder.installOn(inst); | ||
| ClassFileTransformer classFileTransformer = transformerBuilder.installOn(inst); | ||
| if (lambdaTransformationEnabled) { | ||
| registerLambdaTransformer(classFileTransformer, transformerBuilder.lambdaInterfaces()); | ||
| } | ||
| return classFileTransformer; | ||
| } finally { | ||
| SharedTypePools.endInstall(); | ||
| } | ||
| } | ||
|
|
||
| /** Registers the installed class-file transformer for generated lambdas. */ | ||
| private static void registerLambdaTransformer( | ||
| final ClassFileTransformer classFileTransformer, final String[] lambdaInterfaces) { | ||
| LambdaTransformer transformer = | ||
| lambdaInterfaces.length == 0 ? null : newLambdaTransformer(classFileTransformer); | ||
| LambdaTransformerHolder.set(filterLambdaTransformer(transformer, lambdaInterfaces)); | ||
| } | ||
|
|
||
| static LambdaTransformer filterLambdaTransformer( | ||
| final LambdaTransformer transformer, final String[] lambdaInterfaces) { | ||
| if (transformer == null) { | ||
| return null; | ||
| } | ||
| return (className, targetClass, classBytes, interfaceName) -> { | ||
| for (String enabledInterface : lambdaInterfaces) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (enabledInterface.equals(interfaceName)) { | ||
| return transformer.transform(className, targetClass, classBytes, interfaceName); | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Java 9+ requires the module-aware transformer for injected read edges. Failure must disable | ||
| * lambda transformation rather than fall back to the module-less overload. | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| private static LambdaTransformer newLambdaTransformer( | ||
| final ClassFileTransformer classFileTransformer) { | ||
| if (JavaVirtualMachine.isJavaVersionAtLeast(9)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This JDK9+ reflective-loading logic duplicates the existing idiom used elsewhere in this class/ |
||
| try { | ||
| Function<ClassFileTransformer, LambdaTransformer> factory = | ||
| (Function<ClassFileTransformer, LambdaTransformer>) | ||
| Instrumenter.class | ||
| .getClassLoader() | ||
| .loadClass("datadog.trace.agent.tooling.bytebuddy.DDJava9LambdaTransformer") | ||
| .getField("FACTORY") | ||
| .get(null); | ||
| return factory.apply(classFileTransformer); | ||
| } catch (Throwable e) { | ||
| log.debug("Problem loading Java 9 lambda transformer, disabling lambda transformation", e); | ||
| return null; | ||
| } | ||
| } | ||
| // Avoid invoking the instrumented metafactory while installing its transformer. | ||
| return new LambdaTransformer() { | ||
| @Override | ||
| public byte[] transform( | ||
| String slashClassName, | ||
| Class<?> targetClass, | ||
| byte[] classBytes, | ||
| String interfaceClassName) { | ||
| TypePoolFacade.beginLambdaTransform(interfaceClassName); | ||
| try { | ||
| return classFileTransformer.transform( | ||
| targetClass.getClassLoader(), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Affected applications keep the old wrapper and do not preserve Runnable identity. Assertion details
Was this helpful? React 👍 or 👎
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing the target class’s protection domain can be denied. The code safely catches this and falls back. SecurityManager usage is increasingly rare, and IMHO changing the protection domain handling would be a risky change. I would steer not to fix it |
||
| slashClassName, | ||
| null, | ||
| targetClass.getProtectionDomain(), | ||
| classBytes); | ||
| } catch (Throwable ignored) { | ||
| return null; | ||
| } finally { | ||
| TypePoolFacade.endLambdaTransform(); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| /** Returns an iterable that combines the original sequence with any discovered extensions. */ | ||
| private static Iterable<InstrumenterModule> withExtensions(Iterable<InstrumenterModule> initial) { | ||
| String extensionsPath = InstrumenterConfig.get().getTraceExtensionsPath(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,18 +2,22 @@ | |
|
|
||
| import static datadog.trace.api.config.TraceInstrumentationConfig.EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL; | ||
| import static datadog.trace.util.AgentThreadFactory.AgentThread.RETRANSFORMER; | ||
| import static java.util.Collections.unmodifiableMap; | ||
|
|
||
| import datadog.trace.agent.tooling.bytebuddy.matcher.CustomExcludes; | ||
| import datadog.trace.agent.tooling.bytebuddy.matcher.ProxyClassIgnores; | ||
| import datadog.trace.agent.tooling.bytebuddy.outline.TypePoolFacade; | ||
| import datadog.trace.api.InstrumenterConfig; | ||
| import datadog.trace.api.time.TimeUtils; | ||
| import datadog.trace.util.AgentTaskScheduler; | ||
| import java.lang.instrument.Instrumentation; | ||
| import java.security.ProtectionDomain; | ||
| import java.util.ArrayList; | ||
| import java.util.BitSet; | ||
| import java.util.HashMap; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.TimeUnit; | ||
| import net.bytebuddy.agent.builder.AgentBuilder; | ||
|
|
@@ -45,13 +49,22 @@ final class CombiningMatcher implements AgentBuilder.RawMatcher { | |
|
|
||
| private final BitSet knownTypesMask; | ||
| private final MatchRecorder[] matchers; | ||
| private final Map<String, LambdaMatchRecorder[]> lambdaMatchers; | ||
|
|
||
| private volatile boolean deferring; | ||
|
|
||
| CombiningMatcher( | ||
| Instrumentation instrumentation, BitSet knownTypesMask, List<MatchRecorder> matchers) { | ||
| Instrumentation instrumentation, | ||
| BitSet knownTypesMask, | ||
| List<MatchRecorder> matchers, | ||
| Map<String, List<LambdaMatchRecorder>> lambdaMatchers) { | ||
| this.knownTypesMask = knownTypesMask; | ||
| this.matchers = matchers.toArray(new MatchRecorder[0]); | ||
| Map<String, LambdaMatchRecorder[]> lambdaMatchersByInterface = new HashMap<>(); | ||
| lambdaMatchers.forEach( | ||
| (name, recorders) -> | ||
| lambdaMatchersByInterface.put(name, recorders.toArray(new LambdaMatchRecorder[0]))); | ||
| this.lambdaMatchers = unmodifiableMap(lambdaMatchersByInterface); | ||
|
|
||
| if (DEFER_MATCHING) { | ||
| scheduleResumeMatching(instrumentation, InstrumenterConfig.get().deferIntegrationsUntil()); | ||
|
|
@@ -75,6 +88,18 @@ public boolean matches( | |
| ids.clear(); | ||
|
|
||
| long fromTick = InstrumenterMetrics.tick(); | ||
| String lambdaInterface = TypePoolFacade.lambdaInterface(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A lambda linked while |
||
| if (null != lambdaInterface) { | ||
| LambdaMatchRecorder[] recorders = lambdaMatchers.get(lambdaInterface); | ||
| if (null != recorders) { | ||
| for (LambdaMatchRecorder recorder : recorders) { | ||
| recorder.record(target, classLoader, ids); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
| InstrumenterMetrics.matchType(fromTick); | ||
| return !ids.isEmpty(); | ||
| } | ||
|
|
||
| knownTypesIndex.apply(target.getName(), knownTypesMask, ids); | ||
| if (ids.isEmpty()) { | ||
| InstrumenterMetrics.knownTypeMiss(fromTick); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
TRANSFORMINGre-entrancy guard drops field-injection for any lambda whose class definition is triggered as a side effect of processing another lambda's transform on the same thread (e.g. a lambda captured while defining/loading another lambda's supporting classes). That lambda permanently falls back toRunnableWrapperwith only this debug log as a trace.