From 4bd035fe0f3890a0591fa140ec2f71b7f96255ac Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 1 Jul 2026 10:44:21 +0000 Subject: [PATCH 1/3] profiling: request carrier-scoped ddprof context storage on JDK 21+ (PROF-15271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ddprof profiler exposes its OTEL context ThreadContext as a DirectByteBuffer over the carrier thread's native record. When that storage is keyed by the virtual thread (the pre-fix default), a mounted vthread pins to its first carrier: writes land on the wrong carrier after migration, and become a use-after-free once that carrier's OS thread exits. The java-profiler fix (PROF-15271) adds carrier-scoped storage via jdk.internal.misc.CarrierThreadLocal, selected by the ddprof.context.storage.mode system property. Before the profiler is loaded, on JDK 21+ (profiler enabled, non-Windows): - export java.base/jdk.internal.misc to the classloader that loads com.datadoghq.profiler.* so CarrierThreadLocal is reachable, and - set ddprof.context.storage.mode=carrier (only if unset), which requests carrier scoping and makes the profiler fail fast if it cannot honor it. Both actions are inert against ddprof builds that predate carrier support (the property is ignored; the export goes unused), so this is safe to ship ahead of the ddprof version bump — it activates automatically when that lands. Operators opt out with -Dddprof.context.storage.mode=thread (or =auto); we only set the property when unset, so an explicit choice always wins. No separate dd.* config key: the profiler's own system property is the single kill-switch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/datadog/trace/bootstrap/Agent.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) 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..01d4d7501b1 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,47 @@ 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.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.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 {@link #createProfilingContextIntegration()} triggers the profiler load. + */ + 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.context.storage.mode=thread). + if (SystemProperties.get("ddprof.context.storage.mode") == null) { + SystemProperties.set("ddprof.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. From 21833f1047379384fcce2c949fc8d29052dc9ae0 Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 1 Jul 2026 14:09:09 +0200 Subject: [PATCH 2/3] docs: clarify profiler context-storage helper runs before installDatadogTracer Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 01d4d7501b1..9301966c642 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 @@ -1375,7 +1375,8 @@ public void withTracer(TracerAPI tracer) { * *

Operators disable it with {@code -Dddprof.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 {@link #createProfilingContextIntegration()} triggers the profiler load. + * before {@code installDatadogTracer}, which loads the profiler via {@link + * #createProfilingContextIntegration()}. */ private static void prepareDatadogProfilerContextStorage(Instrumentation inst) { try { From 7c7fc8193d7b617520030fdaacf3aa0756c381aa Mon Sep 17 00:00:00 2001 From: Roman Kennke Date: Wed, 1 Jul 2026 16:10:55 +0000 Subject: [PATCH 3/3] profiling: align ddprof context-storage property to ddprof.debug.* (PROF-15271) The java-profiler side renamed the selector to ddprof.debug.context.storage.mode (ddprof.debug.*, signalling an internal knob). Match the property name we export and set here so the carrier request still takes effect. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/java/datadog/trace/bootstrap/Agent.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 9301966c642..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 @@ -1367,13 +1367,13 @@ public void withTracer(TracerAPI tracer) { * 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.context.storage.mode} system property, which it reads at construction. + * 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.context.storage.mode=thread} (or select {@code + *

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()}. @@ -1392,9 +1392,9 @@ private static void prepareDatadogProfilerContextStorage(Instrumentation inst) { 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.context.storage.mode=thread). - if (SystemProperties.get("ddprof.context.storage.mode") == null) { - SystemProperties.set("ddprof.context.storage.mode", "carrier"); + // (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);