Skip to content

Commit 5c40150

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 af8b844 commit 5c40150

6 files changed

Lines changed: 155 additions & 3 deletions

File tree

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
@@ -12,6 +12,7 @@
1212
import com.datadog.debugger.probe.Sampled;
1313
import com.datadog.debugger.sink.DebuggerSink;
1414
import com.datadog.debugger.util.ExceptionHelper;
15+
import datadog.environment.JavaVirtualMachine;
1516
import datadog.trace.api.Config;
1617
import datadog.trace.bootstrap.debugger.DebuggerContext;
1718
import datadog.trace.bootstrap.debugger.ProbeId;
@@ -20,6 +21,9 @@
2021
import datadog.trace.relocate.api.RatelimitedLogger;
2122
import datadog.trace.util.TagsHelper;
2223
import java.lang.instrument.Instrumentation;
24+
import java.lang.reflect.Method;
25+
import java.lang.reflect.Parameter;
26+
import java.util.ArrayList;
2327
import java.util.Collection;
2428
import java.util.Collections;
2529
import java.util.EnumMap;
@@ -40,6 +44,8 @@
4044
*/
4145
public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor {
4246

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

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

2952+
@Test
2953+
public void methodParametersAttribute() throws IOException, URISyntaxException {
2954+
final String CLASS_NAME = "CapturedSnapshot01";
2955+
TestSnapshotListener listener =
2956+
installMethodProbeAtExit(CLASS_NAME, "main", "int (java.lang.String)");
2957+
Map<String, byte[]> buffers =
2958+
compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters"));
2959+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
2960+
int result = Reflect.onClass(testClass).call("main", "1").get();
2961+
assertEquals(3, result);
2962+
if (JavaVirtualMachine.isJavaVersionAtLeast(19)) {
2963+
Snapshot snapshot = assertOneSnapshot(listener);
2964+
assertCaptureArgs(snapshot.getCaptures().getReturn(), "arg", String.class.getTypeName(), "1");
2965+
} else {
2966+
assertEquals(0, listener.snapshots.size());
2967+
ArgumentCaptor<ProbeId> probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class);
2968+
ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);
2969+
verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture());
2970+
assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId());
2971+
assertEquals("Instrumentation fails for CapturedSnapshot01", strCaptor.getAllValues().get(0));
2972+
}
2973+
}
2974+
29512975
private TestSnapshotListener setupInstrumentTheWorldTransformer(
29522976
String excludeFileName, String includeFileName) {
29532977
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)