From b1fb4643b084957661081721eba0fc5715ed47a0 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 14 Apr 2026 19:12:11 +0200 Subject: [PATCH 1/3] feat(agent): Move forced class preload into instrumenter module (cherry picked from commit e95e91e26eb23f70e9128229f0ff6e962ebcac94) --- .../agent/tooling/InstrumenterModule.java | 51 +++++++++---------- .../java/lang/InputStreamInstrumentation.java | 6 +-- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java index fd991e48f59..d2abbc265e5 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java @@ -90,6 +90,7 @@ public int order() { } public List typeInstrumentations() { + preloadClasses(); return singletonList(this); } @@ -214,6 +215,28 @@ protected final boolean isShortcutMatchingEnabled(boolean defaultToShortcut) { .isIntegrationShortcutMatchingEnabled(singletonList(name()), defaultToShortcut); } + /** + * Force loading of classes that need to be instrumented, but are using during instrumentation. + */ + @SuppressForbidden // allow this use of Class.forName() + protected void preloadClasses() { + String[] list = preloadClassNames(); + if (list != null) { + for (String clazz : list) { + try { + Class.forName(clazz); + } catch (Throwable t) { + log.debug("Error force loading {} class", clazz); + } + } + } + } + + /** Get classes to force load */ + public String[] preloadClassNames() { + return null; + } + /** Parent class for all tracing related instrumentations */ public abstract static class Tracing extends InstrumenterModule { public Tracing(String instrumentationName, String... additionalNames) { @@ -258,18 +281,11 @@ public final boolean isApplicable(Set enabledSystems) { } /** Parent class for all IAST related instrumentations */ - @SuppressForbidden public abstract static class Iast extends InstrumenterModule { public Iast(String instrumentationName, String... additionalNames) { super(instrumentationName, additionalNames); } - @Override - public List typeInstrumentations() { - preloadClassNames(); - return super.typeInstrumentations(); - } - @Override public final boolean isApplicable(Set enabledSystems) { if (enabledSystems.contains(TargetSystem.IAST)) { @@ -282,27 +298,6 @@ public final boolean isApplicable(Set enabledSystems) { return cfg.getAppSecActivation() == ProductActivation.FULLY_ENABLED; } - /** - * Force loading of classes that need to be instrumented, but are using during instrumentation. - */ - private void preloadClassNames() { - String[] list = getClassNamesToBePreloaded(); - if (list != null) { - for (String clazz : list) { - try { - Class.forName(clazz); - } catch (Throwable t) { - log.debug("Error force loading {} class", clazz); - } - } - } - } - - /** Get classes to force load* */ - public String[] getClassNamesToBePreloaded() { - return null; - } - @Override public Advice.PostProcessor.Factory postProcessor() { return IastPostProcessorFactory.INSTANCE; diff --git a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/InputStreamInstrumentation.java b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/InputStreamInstrumentation.java index b3802a33c4e..e78f162fffd 100644 --- a/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/InputStreamInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-io-1.8/src/main/java/datadog/trace/instrumentation/java/lang/InputStreamInstrumentation.java @@ -20,7 +20,7 @@ public class InputStreamInstrumentation extends InstrumenterModule.Iast implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { - private static final String[] FORCE_LOADING = {"java.io.PushbackInputStream"}; + private static final String[] PRELOAD_CLASS_NAMES = {"java.io.PushbackInputStream"}; public InputStreamInstrumentation() { super("inputStream"); @@ -44,8 +44,8 @@ public void methodAdvice(MethodTransformer transformer) { } @Override - public String[] getClassNamesToBePreloaded() { - return FORCE_LOADING; + public String[] preloadClassNames() { + return PRELOAD_CLASS_NAMES; } public static class InputStreamAdvice { From 4fd2113b43b6c7ac36d68237f354d0aac543540f Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Tue, 14 Apr 2026 19:12:46 +0200 Subject: [PATCH 2/3] fix(virtual-thread): Preload scope classes to prevent virtual thread deadlock Preload ScopeContext and ScopeStack classes in ContinuableScopeManager constructor to avoid class loading on virtual thread mount path. DatadogClassLoader's synchronized I/O from JarFile can pin carrier threads and deadlock the application. (cherry picked from commit c521b50e1c4e1b23b924a356f539e29f6f023f84) --- .../lang/jdk21/VirtualThreadInstrumentation.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java index d894166e3d4..1caa4ce0038 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java @@ -66,10 +66,22 @@ public final class VirtualThreadInstrumentation extends InstrumenterModule.Conte Instrumenter.HasMethodAdvice, ExcludeFilterProvider { + // Preload classes used by Context.swap() to avoid class loading on the virtual thread mount path. + // DatadogClassLoader loads these from a JarFile using synchronized I/O, which pins + // virtual thread carrier threads and can deadlock the application. + private static final String[] PRELOAD_CLASS_NAMES = { + "datadog.trace.core.scopemanager.ScopeContext", "datadog.trace.core.scopemanager.ScopeStack" + }; + public VirtualThreadInstrumentation() { super("java-lang", "java-lang-21", "virtual-thread"); } + @Override + public String[] preloadClassNames() { + return PRELOAD_CLASS_NAMES; + } + @Override public String instrumentedType() { return VIRTUAL_THREAD_CLASS_NAME; From 34a31990675ff591dbfea95fffcd0207c5a2ce3b Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 16 Apr 2026 10:13:15 +0200 Subject: [PATCH 3/3] fix(virtual-thread): Add support for early VirtualThread afterTerminate method Support both afterDone() and afterTerminate() cleanup methods to handle different JDK 21 virtual thread implementations. Both methods cancel the help continuation and release the context scope when the virtual thread completes. (cherry picked from commit d223f7924b1359dcfe078c2d321f43a4b054381b) --- .../java/lang/jdk21/VirtualThreadInstrumentation.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java index 1caa4ce0038..4f40cc0ff50 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java @@ -45,8 +45,8 @@ *
  • {@code unmount()}: swaps the carrier thread's original context back, saving the virtual * thread's (possibly modified) context for the next mount. *
  • Steps 2-3 repeat on each park/unpark cycle, potentially on different carrier threads. - *
  • {@code afterDone()}: cancels the help continuation, releasing the context scope to be - * closed. + *
  • {@code afterDone()} / {@code afterTerminate()} for early VirtualThread support: cancels the + * help continuation, releasing the context scope to be closed. * * *

    Instrumenting the internal {@code VirtualThread.runContinuation()} method does not work as the @@ -111,6 +111,9 @@ public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( isMethod().and(named("afterDone")).and(takesArguments(boolean.class)), getClass().getName() + "$AfterDone"); + transformer.applyAdvice( + isMethod().and(named("afterTerminate")).and(takesArguments(boolean.class, boolean.class)), + getClass().getName() + "$AfterDone"); } public static final class Construct { @@ -153,7 +156,7 @@ public static void onUnmount(@Advice.This Object virtualThread) { public static final class AfterDone { @OnMethodEnter(suppress = Throwable.class) - public static void onTerminate(@Advice.This Object virtualThread) { + public static void onDone(@Advice.This Object virtualThread) { ContextStore store = InstrumentationContext.get(VIRTUAL_THREAD_CLASS_NAME, VIRTUAL_THREAD_STATE_CLASS_NAME); VirtualThreadState state = store.remove(virtualThread);