From 1e3a9fbd3bc290d25d49401d7971f227855db61a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bempel Date: Thu, 17 Sep 2026 12:03:19 +0200 Subject: [PATCH 1/5] Fix LinkageError for CodeOrigin Avoid trying to transform classes already loading. SymDB transformer and DebuggerTransformer should skip packages from the debugger itself Prevent duplicated class definiton --- .../debugger/agent/DebuggerTransformer.java | 29 +++----------- .../symbol/SymbolExtractionTransformer.java | 7 +++- .../util/DebuggerInternalPackages.java | 33 ++++++++++++++++ .../SymbolExtractionTransformerTest.java | 19 +++++++++ .../util/DebuggerInternalPackagesTest.java | 39 +++++++++++++++++++ 5 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/DebuggerInternalPackages.java create mode 100644 dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/util/DebuggerInternalPackagesTest.java diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index ba70b3dd75b..fdb1ae193a9 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -1,6 +1,7 @@ package com.datadog.debugger.agent; import static com.datadog.debugger.instrumentation.ASMHelper.getLineNumbers; +import static com.datadog.debugger.util.DebuggerInternalPackages.isDebuggerInternalClass; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; @@ -24,6 +25,7 @@ import com.datadog.debugger.sink.SymbolSink; import com.datadog.debugger.uploader.BatchUploader; import com.datadog.debugger.util.ClassFileLines; +import com.datadog.debugger.util.DebuggerInternalPackages; import com.datadog.debugger.util.SpringHelper; import datadog.environment.JavaVirtualMachine; import datadog.environment.SystemProperties; @@ -99,18 +101,6 @@ public class DebuggerTransformer implements ClassFileTransformer { private static final boolean JAVA_AT_LEAST_17_0_20 = JavaVirtualMachine.isJavaVersionAtLeast(17, 0, 20); public static Path DUMP_PATH = Paths.get(SystemProperties.get(JAVA_IO_TMPDIR), "debugger"); - private static final String[] SKIPPED_PACKAGES = - new String[] { - "com/datadog/debugger/agent/", - "com/datadog/debugger/codeorigin/", - "com/datadog/debugger/exception/", - "com/datadog/debugger/instrumentation/", - "com/datadog/debugger/probe/", - "com/datadog/debugger/sink/", - "com/datadog/debugger/symbol/", - "com/datadog/debugger/uploader/", - "com/datadog/debugger/util/" - }; private final Config config; private final TransformerDefinitionMatcher definitionMatcher; @@ -389,17 +379,10 @@ private boolean skipInstrumentation(String classFilePath) { // in case of anonymous classes return true; } - if (classFilePath.startsWith("com/datadog/debugger/")) { - // skip classes/packages that are part of debugger agent to avoid - // LinkageError: attempted duplicate class definition - // while retransforming a class used by instrumentation - for (int i = 0; i < SKIPPED_PACKAGES.length; i++) { - if (classFilePath.startsWith(SKIPPED_PACKAGES[i])) { - return true; - } - } - } - return false; + // skip classes/packages that are part of debugger agent to avoid + // LinkageError: attempted duplicate class definition + // while retransforming a class used by instrumentation + return isDebuggerInternalClass(classFilePath); } private byte[] transformTheWorld( diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java index 0d7b4503c8f..2fe9ee9f9ab 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java @@ -1,5 +1,6 @@ package com.datadog.debugger.symbol; +import com.datadog.debugger.util.DebuggerInternalPackages; import datadog.trace.bootstrap.debugger.DebuggerContext.ClassNameFilter; import datadog.trace.util.Strings; import java.lang.instrument.ClassFileTransformer; @@ -7,6 +8,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static com.datadog.debugger.util.DebuggerInternalPackages.isDebuggerInternalClass; + public class SymbolExtractionTransformer implements ClassFileTransformer { private static final Logger LOGGER = LoggerFactory.getLogger(SymbolExtractionTransformer.class); @@ -31,8 +34,8 @@ public byte[] transform( return null; } try { - if (className.startsWith("com/datadog/debugger/symbol/")) { - // Don't parse our own classes to avoid duplicate class definition + if (isDebuggerInternalClass(className)) { + // Don't parse debugger-internal classes to avoid duplicate class definition return null; } if (classNameFiltering.isExcluded(Strings.getClassName(className))) { diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/DebuggerInternalPackages.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/DebuggerInternalPackages.java new file mode 100644 index 00000000000..62179714b75 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/DebuggerInternalPackages.java @@ -0,0 +1,33 @@ +package com.datadog.debugger.util; + +/** Identifies class files belonging to the debugger's own internal packages. */ +public class DebuggerInternalPackages { + private static final String[] SKIPPED_PACKAGES = { + "com/datadog/debugger/agent/", + "com/datadog/debugger/codeorigin/", + "com/datadog/debugger/exception/", + "com/datadog/debugger/instrumentation/", + "com/datadog/debugger/probe/", + "com/datadog/debugger/sink/", + "com/datadog/debugger/symbol/", + "com/datadog/debugger/uploader/", + "com/datadog/debugger/util/" + }; + + /** + * @param classFilePath slash-separated class file path (e.g. "com/datadog/debugger/agent/Foo") + * @return true if the class belongs to a debugger-internal package that must never be + * re-transformed/parsed, to avoid a re-entrant LinkageError while it is being loaded. + */ + public static boolean isDebuggerInternalClass(String classFilePath) { + if (classFilePath == null || !classFilePath.startsWith("com/datadog/debugger/")) { + return false; + } + for (String pkg : SKIPPED_PACKAGES) { + if (classFilePath.startsWith(pkg)) { + return true; + } + } + return false; + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java index cef56569746..2096d965e16 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/symbol/SymbolExtractionTransformerTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static utils.InstrumentationTestHelper.compileAndLoadClass; @@ -19,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.net.URISyntaxException; import java.net.URL; @@ -1077,6 +1080,22 @@ public void filterOutClassesFromExcludedPackages() throws IOException, URISyntax .anyMatch(scope -> scope.getName().equals(CLASS_NAME))); } + @Test + public void skipDebuggerInternalClasses() throws IllegalClassFormatException { + // Regression test for: DatadogClassLoader attempted duplicate class definition for + // com.datadog.debugger.instrumentation.Types (LinkageError). SymbolExtractionTransformer + // must never parse debugger-internal classes, since doing so while such a class is still + // being loaded/defined can trigger a re-entrant load of the same class on the same thread. + ClassNameFiltering classNameFiltering = new ClassNameFiltering(Collections.emptySet()); + SymbolAggregator symbolAggregator = mock(SymbolAggregator.class); + currentTransformer = new SymbolExtractionTransformer(symbolAggregator, classNameFiltering); + byte[] classfileBuffer = new byte[0]; + assertNull( + currentTransformer.transform( + null, "com/datadog/debugger/instrumentation/Types", null, null, classfileBuffer)); + verifyNoInteractions(symbolAggregator); + } + @Test @DisabledIf( value = "datadog.environment.JavaVirtualMachine#isJ9", diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/util/DebuggerInternalPackagesTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/util/DebuggerInternalPackagesTest.java new file mode 100644 index 00000000000..db0b8b2f0ac --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/util/DebuggerInternalPackagesTest.java @@ -0,0 +1,39 @@ +package com.datadog.debugger.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DebuggerInternalPackagesTest { + + @Test + public void nullIsNotDebuggerInternal() { + assertFalse(DebuggerInternalPackages.isDebuggerInternalClass(null)); + } + + @Test + public void applicationClassIsNotDebuggerInternal() { + assertFalse(DebuggerInternalPackages.isDebuggerInternalClass("com/example/app/MyClass")); + } + + @Test + public void debuggerInstrumentationPackageIsInternal() { + assertTrue( + DebuggerInternalPackages.isDebuggerInternalClass( + "com/datadog/debugger/instrumentation/Types")); + } + + @Test + public void debuggerSymbolPackageIsInternal() { + assertTrue( + DebuggerInternalPackages.isDebuggerInternalClass( + "com/datadog/debugger/symbol/SymbolAggregator")); + } + + @Test + public void debuggerPackageOutsideSkippedListIsNotInternal() { + assertFalse( + DebuggerInternalPackages.isDebuggerInternalClass("com/datadog/debugger/el/Something")); + } +} From 6b20e4ec6bc6b359cef1105505b24b8ef8582db5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bempel Date: Thu, 17 Sep 2026 14:46:55 +0200 Subject: [PATCH 2/5] spotless --- .../java/com/datadog/debugger/agent/DebuggerTransformer.java | 1 - .../datadog/debugger/symbol/SymbolExtractionTransformer.java | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index fdb1ae193a9..68af03f959d 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -25,7 +25,6 @@ import com.datadog.debugger.sink.SymbolSink; import com.datadog.debugger.uploader.BatchUploader; import com.datadog.debugger.util.ClassFileLines; -import com.datadog.debugger.util.DebuggerInternalPackages; import com.datadog.debugger.util.SpringHelper; import datadog.environment.JavaVirtualMachine; import datadog.environment.SystemProperties; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java index 2fe9ee9f9ab..0150cbe02aa 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java @@ -1,6 +1,7 @@ package com.datadog.debugger.symbol; -import com.datadog.debugger.util.DebuggerInternalPackages; +import static com.datadog.debugger.util.DebuggerInternalPackages.isDebuggerInternalClass; + import datadog.trace.bootstrap.debugger.DebuggerContext.ClassNameFilter; import datadog.trace.util.Strings; import java.lang.instrument.ClassFileTransformer; @@ -8,8 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.datadog.debugger.util.DebuggerInternalPackages.isDebuggerInternalClass; - public class SymbolExtractionTransformer implements ClassFileTransformer { private static final Logger LOGGER = LoggerFactory.getLogger(SymbolExtractionTransformer.class); From 636c20699ecf2d41a807f583f04c55b7d4efe9e2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bempel Date: Wed, 23 Sep 2026 14:02:37 +0200 Subject: [PATCH 3/5] fix jackson databind exploration test use mvn command instead of maven wrapper --- .gitlab/exploration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/exploration-tests.yml b/.gitlab/exploration-tests.yml index f8d39acbbbb..099bc4f3849 100644 --- a/.gitlab/exploration-tests.yml +++ b/.gitlab/exploration-tests.yml @@ -123,7 +123,7 @@ exploration-tests-method-jackson-databind: variables: PROJECT: jackson-databind script: - - ./run-exploration-tests.sh "method" "$PROJECT" "./mvnw verify" "include_${PROJECT}.txt" "exclude_$PROJECT.txt" + - ./run-exploration-tests.sh "method" "$PROJECT" "mvn verify" "include_${PROJECT}.txt" "exclude_$PROJECT.txt" exploration-tests-line-jackson-databind: needs: [ build ] From 9ef94512c3d0bbb7356aed009065ec916bd6640b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bempel Date: Wed, 23 Sep 2026 17:18:26 +0200 Subject: [PATCH 4/5] fix DebuggerInternalClass loading make sure DebuggerInternalClass is loaded before executing into a Transformer --- .../java/com/datadog/debugger/agent/DebuggerTransformer.java | 5 +++++ .../datadog/debugger/symbol/SymbolExtractionTransformer.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index 68af03f959d..8b6b2aea7a1 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -173,6 +173,11 @@ public DebuggerTransformer( includeMethods = null; probeCreator = null; } + if (isDebuggerInternalClass(null)) { + // Force DebuggerInternalPAckages to be loaded before calling it into the transform method + // avoid LinkageError for duplicated class definition + throw new IllegalArgumentException("DebuggerInternalClass should be loaded"); + } } // Used only for tests diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java index 0150cbe02aa..9464ea1aa5b 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractionTransformer.java @@ -20,6 +20,11 @@ public SymbolExtractionTransformer( SymbolAggregator symbolAggregator, ClassNameFilter classNameFiltering) { this.symbolAggregator = symbolAggregator; this.classNameFiltering = classNameFiltering; + if (isDebuggerInternalClass(null)) { + // Force DebuggerInternalPAckages to be loaded before calling it into the transform method + // avoid LinkageError for duplicated class definition + throw new IllegalArgumentException("DebuggerInternalClass should be loaded"); + } } @Override From 400c5a2cf6617c81146ac6a2a4310f9f04effc10 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bempel Date: Wed, 23 Sep 2026 19:19:06 +0200 Subject: [PATCH 5/5] fix mvn --- .gitlab/exploration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/exploration-tests.yml b/.gitlab/exploration-tests.yml index 099bc4f3849..99ca4399c23 100644 --- a/.gitlab/exploration-tests.yml +++ b/.gitlab/exploration-tests.yml @@ -133,7 +133,7 @@ exploration-tests-line-jackson-databind: variables: PROJECT: jackson-databind script: - - ./run-exploration-tests.sh "line" "$PROJECT" "./mvnw verify" "include_${PROJECT}.txt" "exclude_line_$PROJECT.txt" + - ./run-exploration-tests.sh "line" "$PROJECT" "mvn verify" "include_${PROJECT}.txt" "exclude_line_$PROJECT.txt" exploration-tests-method-okhttp: needs: [ build ]