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
@@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The TRANSFORMING re-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 to RunnableWrapper with only this debug log as a trace.

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
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

filterLambdaTransformer does a linear scan over lambdaInterfaces per lambda link, on exactly the hot path the PR's own JMH benchmark (LambdaExecutorBenchmark) measures. CombiningTransformerBuilder already has these as a Set/keyed map before it gets flattened to this array — reusing that instead of a per-call linear scan would avoid the O(n) lookup for every lambda linkage.

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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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/AgentStrategies for loading version-specific helper classes reflectively. Worth sharing that helper instead of a second bespoke reflection path.

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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Use agent access for class data

Affected applications keep the old wrapper and do not preserve Runnable identity.

Assertion details
  • Input: Link an application Runnable lambda when an active SecurityManager denies getClassLoader or getProtectionDomain access.
  • Expected: The agent must read the class data with agent access rights.
  • Actual: The class access calls throw SecurityException. The catch returns the original bytes and stops lambda instrumentation.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest · Open Bits AI session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -75,6 +88,18 @@ public boolean matches(
ids.clear();

long fromTick = InstrumenterMetrics.tick();
String lambdaInterface = TypePoolFacade.lambdaInterface();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A lambda linked while EXPERIMENTAL_DEFER_INTEGRATIONS_UNTIL deferral is active returns false here and permanently skips field-injection: wouldIgnore() (line 193) explicitly excludes names containing / ("don't retransform lambdas"), so the later resumeMatching() retransform sweep never revisits it. That's a permanent, silent miss for any lambda linked during the deferral window, not just a delayed one.

if (null != lambdaInterface) {
LambdaMatchRecorder[] recorders = lambdaMatchers.get(lambdaInterface);
if (null != recorders) {
for (LambdaMatchRecorder recorder : recorders) {
recorder.record(target, classLoader, ids);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

recorder.record(target, classLoader, ids) here isn't wrapped in try/catch, unlike the equivalent loop over regular MatchRecorders a few lines below (line 111-112, which logs and continues on Throwable). An exception from a LambdaMatchRecorder propagates uncaught instead of being handled the same way.

}
}
InstrumenterMetrics.matchType(fromTick);
return !ids.isEmpty();
}

knownTypesIndex.apply(target.getName(), knownTypesMask, ids);
if (ids.isEmpty()) {
InstrumenterMetrics.knownTypeMiss(fromTick);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public final class CombiningTransformerBuilder
private final Map<Map.Entry<String, String>, ElementMatcher<ClassLoader>> contextStoreInjection =
new HashMap<>();

private final Map<String, List<LambdaMatchRecorder>> lambdaMatchers = new HashMap<>();
private final Map<Map.Entry<String, String>, List<LambdaMatchRecorder>>
lambdaContextStoreInjection = new HashMap<>();

private final AgentBuilder agentBuilder;
private final InstrumenterIndex instrumenterIndex;
private final int knownTransformationCount;
Expand Down Expand Up @@ -162,9 +166,41 @@ private void buildTypeInstrumentation(Instrumenter member) {
}

buildTypeMatcher(member, transformationId);
buildLambdaMatcher(member, transformationId);
buildTypeAdvice(member, transformationId);
}

private void buildLambdaMatcher(Instrumenter member, int transformationId) {
if (!(member instanceof Instrumenter.ForLambda)) {
return;
}

Instrumenter.ForLambda lambdaInstrumenter = (Instrumenter.ForLambda) member;
ElementMatcher<TypeDescription> typeMatcher = lambdaInstrumenter.lambdaMatcher();
if (member instanceof Instrumenter.WithTypeStructure) {
typeMatcher =
new ElementMatcher.Junction.Conjunction<>(
typeMatcher, ((Instrumenter.WithTypeStructure) member).structureMatcher());
}

LambdaMatchRecorder recorder =
new LambdaMatchRecorder(
transformationId, typeMatcher, requireBoth(classLoaderMatcher, muzzle));
lambdaMatchers
.computeIfAbsent(lambdaInstrumenter.lambdaInterface(), ignored -> new ArrayList<>())
.add(recorder);

for (Map.Entry<String, String> store : contextStore.entrySet()) {
lambdaContextStoreInjection
.computeIfAbsent(store, ignored -> new ArrayList<>())
.add(recorder);
}
}

String[] lambdaInterfaces() {
return lambdaMatchers.keySet().toArray(new String[0]);
}

private void buildTypeMatcher(Instrumenter member, int transformationId) {

if (member instanceof Instrumenter.ForSingleType) {
Expand Down Expand Up @@ -291,7 +327,7 @@ public ClassFileTransformer installOn(Instrumentation instrumentation) {
}

return agentBuilder
.type(new CombiningMatcher(instrumentation, knownTypesMask, matchers))
.type(new CombiningMatcher(instrumentation, knownTypesMask, matchers, lambdaMatchers))
.and(NOT_DECORATOR_MATCHER)
.transform(defaultTransformers())
.transform(new SplittingTransformer(transformers))
Expand Down Expand Up @@ -360,6 +396,15 @@ private void applyContextStoreInjection(

matchers.add(new MatchRecorder.ForContextStore(transformationId, activation, contextMatcher));
transformers[transformationId] = new AdviceStack(new VisitingTransformer(contextAdvice));

List<LambdaMatchRecorder> lambdaRecorders = lambdaContextStoreInjection.get(contextStore);
if (null != lambdaRecorders) {
// Lambda transformation happens before definition, so its field injector can be selected
// directly along with the instrumentation that requested this context store.
for (LambdaMatchRecorder recorder : lambdaRecorders) {
recorder.addTransformation(transformationId);
}
}
}

static final class VisitingTransformer implements AgentBuilder.Transformer {
Expand Down
Loading