Skip to content

Commit 4bd113b

Browse files
jpbempeldevflow.devflow-routing-intake
andauthored
Fix method parameters JVM bug (#10521)
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. disable Scala test for JDK < 21 Add special case for Record canonical record constructor has method parameters attribute add error messages fix test spotless fix test Detect that we are running on Spring6+ spotbugs Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 68b7b69 commit 4bd113b

File tree

13 files changed

+370
-17
lines changed

13 files changed

+370
-17
lines changed

dd-java-agent/agent-debugger/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dependencies {
6767

6868
testRuntimeOnly group: 'org.scala-lang', name: 'scala-compiler', version: libs.versions.scala213.get()
6969
testRuntimeOnly group: 'antlr', name: 'antlr', version: '2.7.7'
70+
testRuntimeOnly group: 'org.springframework', name: 'spring-web', version: '6.0.0'
7071
}
7172

7273
tasks.named("shadowJar", ShadowJar) {

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import com.datadog.debugger.probe.Sampled;
1313
import com.datadog.debugger.sink.DebuggerSink;
1414
import com.datadog.debugger.util.ExceptionHelper;
15+
import com.datadog.debugger.util.SpringHelper;
16+
import datadog.environment.JavaVirtualMachine;
1517
import datadog.trace.api.Config;
1618
import datadog.trace.bootstrap.debugger.DebuggerContext;
1719
import datadog.trace.bootstrap.debugger.ProbeId;
@@ -20,6 +22,9 @@
2022
import datadog.trace.relocate.api.RatelimitedLogger;
2123
import datadog.trace.util.TagsHelper;
2224
import java.lang.instrument.Instrumentation;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Parameter;
27+
import java.util.ArrayList;
2328
import java.util.Collection;
2429
import java.util.Collections;
2530
import java.util.EnumMap;
@@ -40,6 +45,8 @@
4045
*/
4146
public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor {
4247

48+
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
49+
4350
public interface TransformerSupplier {
4451
DebuggerTransformer supply(
4552
Config tracerConfig,
@@ -177,13 +184,63 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne
177184
}
178185
List<Class<?>> changedClasses =
179186
finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes);
187+
changedClasses = detectMethodParameters(changes, changedClasses);
180188
retransformClasses(changedClasses);
181189
// ensures that we have at least re-transformed 1 class
182190
if (changedClasses.size() > 0) {
183191
LOGGER.debug("Re-transformation done");
184192
}
185193
}
186194

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(
202+
ConfigurationComparer changes, List<Class<?>> changedClasses) {
203+
if (JAVA_AT_LEAST_19) {
204+
// bug is fixed since JDK19, no need to perform detection
205+
return changedClasses;
206+
}
207+
List<Class<?>> result = new ArrayList<>();
208+
for (Class<?> changedClass : changedClasses) {
209+
Method[] declaredMethods = changedClass.getDeclaredMethods();
210+
boolean addClass = true;
211+
// capping scanning of methods to 100 to avoid generated class with thousand of methods
212+
// assuming that in those first 100 methods there is at least one with at least one parameter
213+
for (int methodIdx = 0; methodIdx < declaredMethods.length && methodIdx < 100; methodIdx++) {
214+
Method method = declaredMethods[methodIdx];
215+
Parameter[] parameters = method.getParameters();
216+
if (parameters.length == 0) {
217+
continue;
218+
}
219+
if (parameters[0].isNamePresent()) {
220+
if (!SpringHelper.isSpringUsingOnlyMethodParameters(instrumentation)) {
221+
return changedClasses;
222+
}
223+
LOGGER.debug(
224+
"Detecting method parameter: method={} param={}, Skipping retransforming this class",
225+
method.getName(),
226+
parameters[0].getName());
227+
// skip the class: compiled with -parameters
228+
reportError(
229+
changes,
230+
"Method Parameters detected, instrumentation not supported for "
231+
+ changedClass.getTypeName());
232+
addClass = false;
233+
}
234+
// we found at leat a method with one parameter if name is not present we can stop there
235+
break;
236+
}
237+
if (addClass) {
238+
result.add(changedClass);
239+
}
240+
}
241+
return result;
242+
}
243+
187244
private void reportReceived(ConfigurationComparer changes) {
188245
for (ProbeDefinition def : changes.getAddedDefinitions()) {
189246
if (def instanceof ExceptionProbe) {
@@ -197,6 +254,16 @@ private void reportReceived(ConfigurationComparer changes) {
197254
}
198255
}
199256

257+
private void reportError(ConfigurationComparer changes, String errorMsg) {
258+
for (ProbeDefinition def : changes.getAddedDefinitions()) {
259+
if (def instanceof ExceptionProbe) {
260+
// do not report received for exception probes
261+
continue;
262+
}
263+
sink.addError(def.getProbeId(), errorMsg);
264+
}
265+
}
266+
200267
private void installNewDefinitions(Configuration newConfiguration) {
201268
DebuggerContext.initClassFilter(new DenyListHelper(newConfiguration.getDenyList()));
202269
if (appliedDefinitions.isEmpty()) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ public static DebuggerSink getSink() {
463463
return sink;
464464
}
465465

466+
static Instrumentation getInstrumentation() {
467+
return instrumentation;
468+
}
469+
466470
private static DebuggerTransformer createTransformer(
467471
Config config,
468472
Configuration configuration,

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

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static java.util.stream.Collectors.toList;
66

77
import com.datadog.debugger.el.ProbeCondition;
8+
import com.datadog.debugger.instrumentation.ASMHelper;
89
import com.datadog.debugger.instrumentation.DiagnosticMessage;
910
import com.datadog.debugger.instrumentation.InstrumentationResult;
1011
import com.datadog.debugger.instrumentation.MethodInfo;
@@ -24,6 +25,8 @@
2425
import com.datadog.debugger.uploader.BatchUploader;
2526
import com.datadog.debugger.util.ClassFileLines;
2627
import com.datadog.debugger.util.DebuggerMetrics;
28+
import com.datadog.debugger.util.SpringHelper;
29+
import datadog.environment.JavaVirtualMachine;
2730
import datadog.environment.SystemProperties;
2831
import datadog.trace.agent.tooling.AgentStrategies;
2932
import datadog.trace.api.Config;
@@ -61,6 +64,7 @@
6164
import org.objectweb.asm.ClassWriter;
6265
import org.objectweb.asm.MethodVisitor;
6366
import org.objectweb.asm.Opcodes;
67+
import org.objectweb.asm.Type;
6468
import org.objectweb.asm.commons.JSRInlinerAdapter;
6569
import org.objectweb.asm.tree.ClassNode;
6670
import org.objectweb.asm.tree.MethodNode;
@@ -79,7 +83,7 @@
7983
public class DebuggerTransformer implements ClassFileTransformer {
8084
private static final Logger LOGGER = LoggerFactory.getLogger(DebuggerTransformer.class);
8185
private static final String CANNOT_FIND_METHOD = "Cannot find method %s::%s%s";
82-
private static final String INSTRUMENTATION_FAILS = "Instrumentation fails for %s";
86+
private static final String INSTRUMENTATION_FAILS = "Instrumentation failed for %s: %s";
8387
private static final String CANNOT_FIND_LINE = "No executable code was found at %s:L%s";
8488
private static final Pattern COMMA_PATTERN = Pattern.compile(",");
8589
private static final List<Class<?>> PROBE_ORDER =
@@ -90,6 +94,7 @@ public class DebuggerTransformer implements ClassFileTransformer {
9094
SpanDecorationProbe.class,
9195
SpanProbe.class);
9296
private static final String JAVA_IO_TMPDIR = "java.io.tmpdir";
97+
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
9398

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

@@ -258,6 +263,7 @@ public byte[] transform(
258263
return null;
259264
}
260265
ClassNode classNode = parseClassFile(classFilePath, classfileBuffer);
266+
checkMethodParameters(classNode);
261267
boolean transformed =
262268
performInstrumentation(loader, fullyQualifiedClassName, definitions, classNode);
263269
if (transformed) {
@@ -271,11 +277,51 @@ public byte[] transform(
271277
"type {} matched but no transformation for definitions: {}", classFilePath, definitions);
272278
} catch (Throwable ex) {
273279
LOGGER.warn("Cannot transform: ", ex);
274-
reportInstrumentationFails(definitions, fullyQualifiedClassName);
280+
reportInstrumentationFails(definitions, fullyQualifiedClassName, ex.toString());
275281
}
276282
return null;
277283
}
278284

285+
/*
286+
* Because of this bug (https://bugs.openjdk.org/browse/JDK-8240908), classes compiled with
287+
* method parameters (javac -parameters) strip this attribute once retransformed
288+
* Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception
289+
* if no attribute found.
290+
* Note: Even if the attribute is preserved when transforming at load time, the fact that we have
291+
* instrumented the class, we will retransform for removing the instrumentation and then the
292+
* attribute is stripped. That's why we are preventing it even at load time.
293+
*/
294+
private void checkMethodParameters(ClassNode classNode) {
295+
if (JAVA_AT_LEAST_19) {
296+
// bug is fixed since JDK19, no need to perform check
297+
return;
298+
}
299+
boolean isRecord = ASMHelper.isRecord(classNode);
300+
// capping scanning of methods to 100 to avoid generated class with thousand of methods
301+
// assuming that in those first 100 methods there is at least one with at least one parameter
302+
for (int methodIdx = 0; methodIdx < classNode.methods.size() && methodIdx < 100; methodIdx++) {
303+
MethodNode methodNode = classNode.methods.get(methodIdx);
304+
int argumentCount = Type.getArgumentCount(methodNode.desc);
305+
if (argumentCount == 0) {
306+
continue;
307+
}
308+
if (isRecord && methodNode.name.equals("<init>")) {
309+
// skip record constructors, cannot rely on them because of the canonical one
310+
// use the equals method for this
311+
continue;
312+
}
313+
if (methodNode.parameters != null
314+
&& !methodNode.parameters.isEmpty()
315+
&& SpringHelper.isSpringUsingOnlyMethodParameters(DebuggerAgent.getInstrumentation())) {
316+
throw new RuntimeException(
317+
"Method Parameters attribute detected, instrumentation not supported");
318+
} else {
319+
// we found at leat a method with one parameter if name is not present we can stop there
320+
break;
321+
}
322+
}
323+
}
324+
279325
private boolean skipInstrumentation(ClassLoader loader, String classFilePath) {
280326
if (definitionMatcher.isEmpty()) {
281327
LOGGER.debug("No debugger definitions present.");
@@ -492,7 +538,7 @@ private byte[] writeClassFile(
492538
classNode.accept(visitor);
493539
} catch (Throwable t) {
494540
LOGGER.error("Cannot write classfile for class: {} Exception: ", classFilePath, t);
495-
reportInstrumentationFails(definitions, Strings.getClassName(classFilePath));
541+
reportInstrumentationFails(definitions, Strings.getClassName(classFilePath), t.toString());
496542
return null;
497543
}
498544
byte[] data = writer.toByteArray();
@@ -616,8 +662,9 @@ private void reportLocationNotFound(
616662
// on a separate class files because probe was set on an inner/top-level class
617663
}
618664

619-
private void reportInstrumentationFails(List<ProbeDefinition> definitions, String className) {
620-
String msg = String.format(INSTRUMENTATION_FAILS, className);
665+
private void reportInstrumentationFails(
666+
List<ProbeDefinition> definitions, String className, String errorMsg) {
667+
String msg = String.format(INSTRUMENTATION_FAILS, className, errorMsg);
621668
reportErrorForAllProbes(definitions, msg);
622669
}
623670

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ public static boolean isFinalField(Field field) {
138138
return Modifier.isFinal(field.getModifiers());
139139
}
140140

141+
public static boolean isRecord(ClassNode classNode) {
142+
return (classNode.access & Opcodes.ACC_RECORD) != 0;
143+
}
144+
141145
public static void invokeStatic(
142146
InsnList insnList,
143147
org.objectweb.asm.Type owner,

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/sink/DebuggerSink.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ public void addBlocked(ProbeId probeId) {
208208
probeStatusSink.addBlocked(probeId);
209209
}
210210

211+
public void addError(ProbeId probeId, String msg) {
212+
probeStatusSink.addError(probeId, msg);
213+
}
214+
211215
public void removeDiagnostics(ProbeId probeId) {
212216
probeStatusSink.removeDiagnostics(probeId);
213217
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.datadog.debugger.util;
2+
3+
import java.lang.instrument.Instrumentation;
4+
5+
public class SpringHelper {
6+
7+
public static boolean isSpringUsingOnlyMethodParameters(Instrumentation inst) {
8+
for (Class<?> clazz : inst.getAllLoadedClasses()) {
9+
if ("org.springframework.web.bind.annotation.ControllerMappingReflectiveProcessor"
10+
.equals(clazz.getName())) {
11+
// If this class (coming from Spring web since version 6) is found loaded it means Spring
12+
// supports only getting parameter names from the MethodParameter attribute
13+
return true;
14+
}
15+
}
16+
// class not found, probably no Spring
17+
return false;
18+
}
19+
}

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

Lines changed: 71 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;
@@ -64,6 +65,7 @@
6465
import java.io.File;
6566
import java.io.FileNotFoundException;
6667
import java.io.IOException;
68+
import java.lang.instrument.Instrumentation;
6769
import java.net.URISyntaxException;
6870
import java.net.URL;
6971
import java.util.ArrayList;
@@ -598,6 +600,7 @@ public void methodProbeLineProbeMix() throws IOException, URISyntaxException {
598600
}
599601

600602
@Test
603+
@EnabledForJreRange(min = JRE.JAVA_21)
601604
public void sourceFileProbeScala() throws IOException, URISyntaxException {
602605
final String CLASS_NAME = "CapturedSnapshot101";
603606
final String FILE_NAME = CLASS_NAME + SCALA_EXT;
@@ -2948,6 +2951,74 @@ public void captureExpressionsWithCaptureLimits() throws IOException, URISyntaxE
29482951
assertEquals("depth", fldValue.getNotCapturedReason());
29492952
}
29502953

2954+
@Test
2955+
public void methodParametersAttribute() throws Exception {
2956+
final String CLASS_NAME = "CapturedSnapshot01";
2957+
Config config = mock(Config.class);
2958+
when(config.isDebuggerCodeOriginEnabled()).thenReturn(false);
2959+
when(config.isDebuggerExceptionEnabled()).thenReturn(false);
2960+
when(config.isDynamicInstrumentationEnabled()).thenReturn(false);
2961+
Instrumentation inst = mock(Instrumentation.class);
2962+
if (JavaVirtualMachine.isJavaVersion(17)) {
2963+
// on JDK 17 introduced Spring6 class
2964+
Class<?> springClass =
2965+
Class.forName(
2966+
"org.springframework.web.bind.annotation.ControllerMappingReflectiveProcessor");
2967+
when(inst.getAllLoadedClasses()).thenReturn(new Class[] {springClass});
2968+
} else {
2969+
when(inst.getAllLoadedClasses()).thenReturn(new Class[0]);
2970+
}
2971+
DebuggerAgent.run(config, inst, null);
2972+
TestSnapshotListener listener =
2973+
installMethodProbeAtExit(CLASS_NAME, "main", "int (java.lang.String)");
2974+
Map<String, byte[]> buffers =
2975+
compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters"));
2976+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
2977+
int result = Reflect.onClass(testClass).call("main", "1").get();
2978+
assertEquals(3, result);
2979+
if (JavaVirtualMachine.isJavaVersion(17)) {
2980+
// on JDK 17 with Spring6 class, transformation cannot happen
2981+
assertEquals(0, listener.snapshots.size());
2982+
ArgumentCaptor<ProbeId> probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class);
2983+
ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);
2984+
verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture());
2985+
assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId());
2986+
assertEquals(
2987+
"Instrumentation failed for CapturedSnapshot01: java.lang.RuntimeException: Method Parameters attribute detected, instrumentation not supported",
2988+
strCaptor.getAllValues().get(0));
2989+
} else {
2990+
Snapshot snapshot = assertOneSnapshot(listener);
2991+
assertCaptureArgs(snapshot.getCaptures().getReturn(), "arg", String.class.getTypeName(), "1");
2992+
}
2993+
}
2994+
2995+
@Test
2996+
@EnabledForJreRange(min = JRE.JAVA_17)
2997+
public void methodParametersAttributeRecord() throws IOException, URISyntaxException {
2998+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot29";
2999+
final String RECORD_NAME = "com.datadog.debugger.MyRecord1";
3000+
TestSnapshotListener listener = installMethodProbeAtExit(RECORD_NAME, "<init>", null);
3001+
Map<String, byte[]> buffers =
3002+
compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "17", Arrays.asList("-parameters"));
3003+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
3004+
int result = Reflect.onClass(testClass).call("main", "1").get();
3005+
assertEquals(42, result);
3006+
if (JavaVirtualMachine.isJavaVersionAtLeast(19)) {
3007+
Snapshot snapshot = assertOneSnapshot(listener);
3008+
assertCaptureArgs(
3009+
snapshot.getCaptures().getReturn(), "firstName", String.class.getTypeName(), "john");
3010+
} else {
3011+
assertEquals(0, listener.snapshots.size());
3012+
ArgumentCaptor<ProbeId> probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class);
3013+
ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);
3014+
verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture());
3015+
assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId());
3016+
assertEquals(
3017+
"Instrumentation failed for com.datadog.debugger.MyRecord1: java.lang.RuntimeException: Method Parameters attribute detected, instrumentation not supported",
3018+
strCaptor.getAllValues().get(0));
3019+
}
3020+
}
3021+
29513022
private TestSnapshotListener setupInstrumentTheWorldTransformer(
29523023
String excludeFileName, String includeFileName) {
29533024
Config config = mock(Config.class);

0 commit comments

Comments
 (0)