diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index aae283da55c..be0e232709d 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -60,6 +60,7 @@ import datadog.trace.bootstrap.instrumentation.jfr.InstrumentationBasedProfiling; import datadog.trace.util.AgentTaskScheduler; import datadog.trace.util.AgentThreadFactory.AgentThread; +import datadog.trace.util.JDK9ModuleAccess; import datadog.trace.util.throwable.FatalAgentMisconfigurationError; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.lang.instrument.Instrumentation; @@ -659,6 +660,8 @@ public InstallDatadogTracerCallback( } installDatadogMeter(initTelemetry); + // Must run before installDatadogTracer, which triggers the ddprof profiler load. + prepareDatadogProfilerContextStorage(instrumentation); installDatadogTracer(initTelemetry, scoClass, sco); maybeInstallLogsIntake(scoClass, sco); maybeStartIast(instrumentation); @@ -1356,6 +1359,48 @@ public void withTracer(TracerAPI tracer) { }); } + /** + * Prepares carrier-scoped OTEL context storage for the Datadog profiler before it is loaded. + * + *

On JDK 21+, the profiler can scope its context {@code ThreadContext} storage to the carrier + * thread using {@code jdk.internal.misc.CarrierThreadLocal}, so a mounted virtual thread resolves + * to its current carrier's record — fixing a virtual-thread context use-after-free. That type + * lives in a non-exported package, so we export it to the classloader that loads {@code + * com.datadoghq.profiler.*}, and request carrier scoping via the profiler's {@code + * ddprof.debug.context.storage.mode} system property, which it reads at construction. + * + *

Both actions are inert against profiler builds that predate carrier support (the property is + * ignored; the export goes unused), so this is safe to ship ahead of the corresponding + * java-profiler version bump — it simply activates when that lands. + * + *

Operators disable it with {@code -Dddprof.debug.context.storage.mode=thread} (or select {@code + * auto}); we only set the property when it is unset, so an explicit choice always wins. Must run + * before {@code installDatadogTracer}, which loads the profiler via {@link + * #createProfilingContextIntegration()}. + */ + private static void prepareDatadogProfilerContextStorage(Instrumentation inst) { + try { + if (inst == null + || !Config.get().isProfilingEnabled() + || !Config.get().isDatadogProfilerEnabled() + || OperatingSystem.isWindows() + || !isJavaVersionAtLeast(21)) { + return; + } + // Export jdk.internal.misc to the profiler's classloader so CarrierThreadLocal is reachable. + // Harmless when unused (e.g. thread mode, or a profiler build without carrier support). + JDK9ModuleAccess.exportModuleToUnnamedModule( + inst, "java.base", new String[] {"jdk.internal.misc"}, AGENT_CLASSLOADER); + // Request carrier scoping unless an operator has explicitly set the profiler's mode property + // (which also serves as the kill-switch: -Dddprof.debug.context.storage.mode=thread). + if (SystemProperties.get("ddprof.debug.context.storage.mode") == null) { + SystemProperties.set("ddprof.debug.context.storage.mode", "carrier"); + } + } catch (Throwable t) { + log.debug("Unable to prepare carrier-scoped profiler context storage", t); + } + } + /** * {@see com.datadog.profiling.ddprof.DatadogProfilingIntegration} must not be modified to depend * on JFR.