Skip to content

Commit 633214e

Browse files
committed
Fix method parameters JVM bug
Prevent classfiles with Method Parameters attribute (javac -parameters) to be (re)transformed when on JDK < 19. Spring 6+ or SpringBoot3+ rely exclusively on method parameters to get param name and if the attribute is not present throw an exception and returns 500 on endpoints. see OpenJDK bug JDK-8240908. we are scanning method with at least on parameter to detect if the class was compiled with method parameters attribute and if the JDK is < 19 we prevent instrumentation to happen. Even at load time we prevent it because we need to call retransform to remove instrumentation. Therefore the attribute can be strip at that time.
1 parent f9f47f3 commit 633214e

File tree

6 files changed

+155
-3
lines changed

6 files changed

+155
-3
lines changed

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.datadog.debugger.probe.Sampling;
1414
import com.datadog.debugger.sink.DebuggerSink;
1515
import com.datadog.debugger.util.ExceptionHelper;
16+
import datadog.environment.JavaVirtualMachine;
1617
import datadog.trace.api.Config;
1718
import datadog.trace.bootstrap.debugger.DebuggerContext;
1819
import datadog.trace.bootstrap.debugger.ProbeId;
@@ -21,6 +22,9 @@
2122
import datadog.trace.relocate.api.RatelimitedLogger;
2223
import datadog.trace.util.TagsHelper;
2324
import java.lang.instrument.Instrumentation;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Parameter;
27+
import java.util.ArrayList;
2428
import java.util.Collection;
2529
import java.util.Collections;
2630
import java.util.EnumMap;
@@ -41,6 +45,8 @@
4145
*/
4246
public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor {
4347

48+
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
49+
4450
public interface TransformerSupplier {
4551
DebuggerTransformer supply(
4652
Config tracerConfig,
@@ -178,13 +184,55 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne
178184
}
179185
List<Class<?>> changedClasses =
180186
finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes);
187+
changedClasses = detectMethodParameters(changedClasses);
181188
retransformClasses(changedClasses);
182189
// ensures that we have at least re-transformed 1 class
183190
if (changedClasses.size() > 0) {
184191
LOGGER.debug("Re-transformation done");
185192
}
186193
}
187194

195+
/*
196+
* Because of this bug (https://bugs.openjdk.org/browse/JDK-8240908), classes compiled with
197+
* method parameters (javac -parameters) strip this attribute once retransformed
198+
* Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception
199+
* if no attribute found.
200+
*/
201+
private List<Class<?>> detectMethodParameters(List<Class<?>> changedClasses) {
202+
if (JAVA_AT_LEAST_19) {
203+
// bug is fixed since JDK19, no need to perform detection
204+
return changedClasses;
205+
}
206+
List<Class<?>> result = new ArrayList<>();
207+
for (Class<?> changedClass : changedClasses) {
208+
Method[] declaredMethods = changedClass.getDeclaredMethods();
209+
boolean addClass = true;
210+
// capping scanning of methods to 100 to avoid generated class with thousand of methods
211+
// assuming that in those first 100 methods there is at least one with at least one parameter
212+
for (int methodIdx = 0; methodIdx < declaredMethods.length && methodIdx < 100; methodIdx++) {
213+
Method method = declaredMethods[methodIdx];
214+
Parameter[] parameters = method.getParameters();
215+
if (parameters.length == 0) {
216+
continue;
217+
}
218+
if (parameters[0].isNamePresent()) {
219+
LOGGER.debug(
220+
"Detecting method parameter: method={} param={}, Skipping retransforming this class",
221+
method.getName(),
222+
parameters[0].getName());
223+
// skip the class: compiled with -parameters
224+
addClass = false;
225+
}
226+
// we found at leat a method with one parameter if name is not present we can stop there
227+
break;
228+
}
229+
if (addClass) {
230+
result.add(changedClass);
231+
}
232+
}
233+
return result;
234+
}
235+
188236
private void reportReceived(ConfigurationComparer changes) {
189237
for (ProbeDefinition def : changes.getAddedDefinitions()) {
190238
if (def instanceof ExceptionProbe) {

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.datadog.debugger.uploader.BatchUploader;
2525
import com.datadog.debugger.util.ClassFileLines;
2626
import com.datadog.debugger.util.DebuggerMetrics;
27+
import datadog.environment.JavaVirtualMachine;
2728
import datadog.environment.SystemProperties;
2829
import datadog.trace.agent.tooling.AgentStrategies;
2930
import datadog.trace.api.Config;
@@ -61,6 +62,7 @@
6162
import org.objectweb.asm.ClassWriter;
6263
import org.objectweb.asm.MethodVisitor;
6364
import org.objectweb.asm.Opcodes;
65+
import org.objectweb.asm.Type;
6466
import org.objectweb.asm.commons.JSRInlinerAdapter;
6567
import org.objectweb.asm.tree.ClassNode;
6668
import org.objectweb.asm.tree.MethodNode;
@@ -90,6 +92,7 @@ public class DebuggerTransformer implements ClassFileTransformer {
9092
SpanDecorationProbe.class,
9193
SpanProbe.class);
9294
private static final String JAVA_IO_TMPDIR = "java.io.tmpdir";
95+
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
9396

9497
public static Path DUMP_PATH = Paths.get(SystemProperties.get(JAVA_IO_TMPDIR), "debugger");
9598

@@ -258,6 +261,7 @@ public byte[] transform(
258261
return null;
259262
}
260263
ClassNode classNode = parseClassFile(classFilePath, classfileBuffer);
264+
checkMethodParameters(classNode);
261265
boolean transformed =
262266
performInstrumentation(loader, fullyQualifiedClassName, definitions, classNode);
263267
if (transformed) {
@@ -276,6 +280,38 @@ public byte[] transform(
276280
return null;
277281
}
278282

283+
/*
284+
* Because of this bug (https://bugs.openjdk.org/browse/JDK-8240908), classes compiled with
285+
* method parameters (javac -parameters) strip this attribute once retransformed
286+
* Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception
287+
* if no attribute found.
288+
* Note: Even if the attribute is preserved when transforming at load time, the fact that we have
289+
* instrumented the class, we will retransform for removing the instrumentation and then the
290+
* attribute is stripped. That's why we are preventing it even at load time.
291+
*/
292+
private void checkMethodParameters(ClassNode classNode) {
293+
if (JAVA_AT_LEAST_19) {
294+
// bug is fixed since JDK19, no need to perform check
295+
return;
296+
}
297+
// capping scanning of methods to 100 to avoid generated class with thousand of methods
298+
// assuming that in those first 100 methods there is at least one with at least one parameter
299+
for (int methodIdx = 0; methodIdx < classNode.methods.size() && methodIdx < 100; methodIdx++) {
300+
MethodNode methodNode = classNode.methods.get(methodIdx);
301+
int argumentCount = Type.getArgumentCount(methodNode.desc);
302+
if (argumentCount == 0) {
303+
continue;
304+
}
305+
if (methodNode.parameters != null && !methodNode.parameters.isEmpty()) {
306+
throw new RuntimeException(
307+
"Method Parameters attribute detected, cannot instrument class " + classNode.name);
308+
} else {
309+
// we found at leat a method with one parameter if name is not present we can stop there
310+
break;
311+
}
312+
}
313+
}
314+
279315
private boolean skipInstrumentation(ClassLoader loader, String classFilePath) {
280316
if (definitionMatcher.isEmpty()) {
281317
LOGGER.debug("No debugger definitions present.");

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.datadog.debugger.util.MoshiSnapshotTestHelper;
4545
import com.datadog.debugger.util.TestSnapshotListener;
4646
import com.datadog.debugger.util.TestTraceInterceptor;
47+
import datadog.environment.JavaVirtualMachine;
4748
import datadog.trace.agent.tooling.TracerInstaller;
4849
import datadog.trace.api.Config;
4950
import datadog.trace.api.interceptor.MutableSpan;
@@ -2807,6 +2808,29 @@ public void captureExpressionsWithCaptureLimits() throws IOException, URISyntaxE
28072808
assertEquals("depth", fldValue.getNotCapturedReason());
28082809
}
28092810

2811+
@Test
2812+
public void methodParametersAttribute() throws IOException, URISyntaxException {
2813+
final String CLASS_NAME = "CapturedSnapshot01";
2814+
TestSnapshotListener listener =
2815+
installMethodProbeAtExit(CLASS_NAME, "main", "int (java.lang.String)");
2816+
Map<String, byte[]> buffers =
2817+
compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters"));
2818+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
2819+
int result = Reflect.onClass(testClass).call("main", "1").get();
2820+
assertEquals(3, result);
2821+
if (JavaVirtualMachine.isJavaVersionAtLeast(19)) {
2822+
Snapshot snapshot = assertOneSnapshot(listener);
2823+
assertCaptureArgs(snapshot.getCaptures().getReturn(), "arg", String.class.getTypeName(), "1");
2824+
} else {
2825+
assertEquals(0, listener.snapshots.size());
2826+
ArgumentCaptor<ProbeId> probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class);
2827+
ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);
2828+
verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture());
2829+
assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId());
2830+
assertEquals("Instrumentation fails for CapturedSnapshot01", strCaptor.getAllValues().get(0));
2831+
}
2832+
}
2833+
28102834
private TestSnapshotListener setupInstrumentTheWorldTransformer(
28112835
String excludeFileName, String includeFileName) {
28122836
Config config = mock(Config.class);

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import static org.mockito.Mockito.verify;
1414
import static org.mockito.Mockito.verifyNoInteractions;
1515
import static org.mockito.Mockito.when;
16+
import static utils.InstrumentationTestHelper.compile;
17+
import static utils.InstrumentationTestHelper.loadClass;
1618

1719
import com.datadog.debugger.el.DSL;
1820
import com.datadog.debugger.el.ProbeCondition;
@@ -23,11 +25,14 @@
2325
import com.datadog.debugger.probe.SpanProbe;
2426
import com.datadog.debugger.sink.DebuggerSink;
2527
import com.datadog.debugger.sink.ProbeStatusSink;
28+
import datadog.environment.JavaVirtualMachine;
2629
import datadog.trace.api.Config;
2730
import datadog.trace.bootstrap.debugger.ProbeId;
2831
import datadog.trace.bootstrap.debugger.ProbeImplementation;
32+
import java.io.IOException;
2933
import java.lang.instrument.Instrumentation;
3034
import java.lang.instrument.UnmodifiableClassException;
35+
import java.net.URISyntaxException;
3136
import java.util.*;
3237
import java.util.concurrent.atomic.AtomicInteger;
3338
import java.util.function.Function;
@@ -39,6 +44,7 @@
3944
import org.mockito.ArgumentCaptor;
4045
import org.mockito.Mock;
4146
import org.mockito.junit.jupiter.MockitoExtension;
47+
import utils.SourceCompiler;
4248

4349
@ExtendWith(MockitoExtension.class)
4450
public class ConfigurationUpdaterTest {
@@ -623,6 +629,30 @@ public void handleException() {
623629
verify(probeStatusSink).addError(eq(ProbeId.from(PROBE_ID.getId() + ":0")), eq(ex));
624630
}
625631

632+
@Test
633+
public void methodParametersAttribute()
634+
throws IOException, URISyntaxException, UnmodifiableClassException {
635+
final String CLASS_NAME = "CapturedSnapshot01";
636+
Map<String, byte[]> buffers =
637+
compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters"));
638+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
639+
when(inst.getAllLoadedClasses()).thenReturn(new Class[] {testClass});
640+
ConfigurationUpdater configurationUpdater = createConfigUpdater(debuggerSinkWithMockStatusSink);
641+
configurationUpdater.accept(
642+
REMOTE_CONFIG,
643+
singletonList(
644+
LogProbe.builder().probeId(PROBE_ID).where("CapturedSnapshot01", "main").build()));
645+
if (JavaVirtualMachine.isJavaVersionAtLeast(19)) {
646+
ArgumentCaptor<Class<?>[]> captor = ArgumentCaptor.forClass(Class[].class);
647+
verify(inst, times(1)).retransformClasses(captor.capture());
648+
List<Class<?>[]> allValues = captor.getAllValues();
649+
assertEquals(testClass, allValues.get(0));
650+
} else {
651+
verify(inst).getAllLoadedClasses();
652+
verify(inst, times(0)).retransformClasses(any());
653+
}
654+
}
655+
626656
private DebuggerTransformer createTransformer(
627657
Config tracerConfig,
628658
Configuration configuration,

dd-java-agent/agent-debugger/src/test/java/utils/InstrumentationTestHelper.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.net.URLClassLoader;
1313
import java.nio.file.Files;
1414
import java.nio.file.Paths;
15+
import java.util.Collections;
1516
import java.util.HashMap;
1617
import java.util.List;
1718
import java.util.Map;
@@ -52,8 +53,17 @@ public static Map<String, byte[]> compile(String className, String version)
5253
public static Map<String, byte[]> compile(
5354
String className, SourceCompiler.DebugInfo debugInfo, String version)
5455
throws IOException, URISyntaxException {
56+
return compile(className, debugInfo, version, Collections.emptyList());
57+
}
58+
59+
public static Map<String, byte[]> compile(
60+
String className,
61+
SourceCompiler.DebugInfo debugInfo,
62+
String version,
63+
List<String> additionalOptions)
64+
throws IOException, URISyntaxException {
5565
String classSource = getFixtureContent("/" + className.replace('.', '/') + ".java");
56-
return SourceCompiler.compile(className, classSource, debugInfo, version);
66+
return SourceCompiler.compile(className, classSource, debugInfo, version, additionalOptions);
5767
}
5868

5969
public static Class<?> loadClass(String className, String classFileName) throws IOException {

dd-java-agent/agent-debugger/src/test/java/utils/SourceCompiler.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ public enum DebugInfo {
1818
}
1919

2020
public static Map<String, byte[]> compile(
21-
String className, String source, DebugInfo debug, String version) {
21+
String className,
22+
String source,
23+
DebugInfo debug,
24+
String version,
25+
List<String> additionalOptions) {
2226
JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
2327
if (jc == null) throw new RuntimeException("Compiler unavailable");
2428

2529
JavaSourceFromString jsfs = new JavaSourceFromString(className, source);
2630

2731
Iterable<? extends JavaFileObject> fileObjects = Collections.singletonList(jsfs);
2832

29-
List<String> options = new ArrayList<>();
33+
List<String> options = new ArrayList<>(additionalOptions);
3034
switch (debug) {
3135
case ALL:
3236
{

0 commit comments

Comments
 (0)