Skip to content

Commit 344c6d2

Browse files
committed
Prevent retransforming record with type annotation
To avoid JVM bug, we detect and prevent retransformation of record with type annotation on record component. see https://bugs.openjdk.org/browse/JDK-8376185
1 parent fd65c0a commit 344c6d2

5 files changed

Lines changed: 184 additions & 4 deletions

File tree

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

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@
2121
import datadog.trace.bootstrap.debugger.ProbeRateLimiter;
2222
import datadog.trace.relocate.api.RatelimitedLogger;
2323
import datadog.trace.util.TagsHelper;
24+
import java.lang.annotation.Annotation;
25+
import java.lang.annotation.ElementType;
26+
import java.lang.annotation.Target;
2427
import java.lang.instrument.Instrumentation;
28+
import java.lang.reflect.AnnotatedType;
29+
import java.lang.reflect.Array;
2530
import java.lang.reflect.Method;
31+
import java.lang.reflect.Modifier;
2632
import java.lang.reflect.Parameter;
2733
import java.util.ArrayList;
34+
import java.util.Arrays;
2835
import java.util.Collection;
2936
import java.util.Collections;
3037
import java.util.EnumMap;
@@ -44,8 +51,29 @@
4451
* re-transformation of required classes
4552
*/
4653
public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor {
47-
54+
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUpdater.class);
55+
private static final int MINUTES_BETWEEN_ERROR_LOG = 5;
4856
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
57+
private static final boolean JAVA_AT_LEAST_16 = JavaVirtualMachine.isJavaVersionAtLeast(16);
58+
private static final Method GET_RECORD_COMPONENTS_METHOD;
59+
private static final Method GET_ANNOTATED_TYPES_METHOD;
60+
61+
static {
62+
Method getRecordComponentsMethod = null;
63+
Method getAnnotatedTypesMethod = null;
64+
if (JAVA_AT_LEAST_16) {
65+
try {
66+
Class<?> recordClass = Class.forName("java.lang.Record");
67+
getRecordComponentsMethod = recordClass.getClass().getDeclaredMethod("getRecordComponents");
68+
Class<?> recordComponentClass = Class.forName("java.lang.reflect.RecordComponent");
69+
getAnnotatedTypesMethod = recordComponentClass.getDeclaredMethod("getAnnotatedType");
70+
} catch (Exception e) {
71+
LOGGER.debug("Exception initializing reflection constants", e);
72+
}
73+
}
74+
GET_RECORD_COMPONENTS_METHOD = getRecordComponentsMethod;
75+
GET_ANNOTATED_TYPES_METHOD = getAnnotatedTypesMethod;
76+
}
4977

5078
public interface TransformerSupplier {
5179
DebuggerTransformer supply(
@@ -56,9 +84,6 @@ DebuggerTransformer supply(
5684
DebuggerSink debuggerSink);
5785
}
5886

59-
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUpdater.class);
60-
private static final int MINUTES_BETWEEN_ERROR_LOG = 5;
61-
6287
private final Instrumentation instrumentation;
6388
private final TransformerSupplier transformerSupplier;
6489
private final Lock configurationLock = new ReentrantLock();
@@ -185,6 +210,7 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne
185210
List<Class<?>> changedClasses =
186211
finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes);
187212
changedClasses = detectMethodParameters(changes, changedClasses);
213+
changedClasses = detectRecordWithTypeAnnotation(changes, changedClasses);
188214
retransformClasses(changedClasses);
189215
// ensures that we have at least re-transformed 1 class
190216
if (changedClasses.size() > 0) {
@@ -248,6 +274,65 @@ private List<Class<?>> detectMethodParameters(
248274
return result;
249275
}
250276

277+
private List<Class<?>> detectRecordWithTypeAnnotation(
278+
ConfigurationComparer changes, List<Class<?>> changedClasses) {
279+
if (!JAVA_AT_LEAST_16) {
280+
// records introduced in JDK 16 (final version)
281+
return changedClasses;
282+
}
283+
List<Class<?>> result = new ArrayList<>();
284+
for (Class<?> changedClass : changedClasses) {
285+
boolean addClass = true;
286+
try {
287+
if (changedClass.getSuperclass().getTypeName().equals("java.lang.Record")
288+
&& Modifier.isFinal(changedClass.getModifiers())) {
289+
if (hasTypeAnnotationOnRecordComponent(changedClass)) {
290+
LOGGER.debug(
291+
"Record with type annotation detected, instrumentation not supported for {}",
292+
changedClass.getTypeName());
293+
reportError(
294+
changes,
295+
"Record with type annotation detected, instrumentation not supported for "
296+
+ changedClass.getTypeName());
297+
addClass = false;
298+
}
299+
}
300+
} catch (Exception e) {
301+
LOGGER.debug("Exception detecting record with type annotation", e);
302+
}
303+
if (addClass) {
304+
result.add(changedClass);
305+
}
306+
}
307+
return result;
308+
}
309+
310+
private boolean hasTypeAnnotationOnRecordComponent(Class<?> recordClass) {
311+
if (GET_RECORD_COMPONENTS_METHOD == null || GET_ANNOTATED_TYPES_METHOD == null) {
312+
return false;
313+
}
314+
try {
315+
Object recordComponentsArray = GET_RECORD_COMPONENTS_METHOD.invoke(recordClass);
316+
int len = Array.getLength(recordComponentsArray);
317+
for (int i = 0; i < len; i++) {
318+
Object recordComponent = Array.get(recordComponentsArray, i);
319+
AnnotatedType annotatedType =
320+
(AnnotatedType) GET_ANNOTATED_TYPES_METHOD.invoke(recordComponent);
321+
for (Annotation annotation : annotatedType.getAnnotations()) {
322+
Target annotationTarget = annotation.annotationType().getAnnotation(Target.class);
323+
if (annotationTarget != null
324+
&& Arrays.stream(annotationTarget.value())
325+
.anyMatch(it -> it == ElementType.TYPE_USE)) {
326+
return true;
327+
}
328+
}
329+
}
330+
return false;
331+
} catch (Exception ex) {
332+
return false;
333+
}
334+
}
335+
251336
private void reportReceived(ConfigurationComparer changes) {
252337
for (ProbeDefinition def : changes.getAddedDefinitions()) {
253338
if (def instanceof ExceptionProbe) {

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.objectweb.asm.Type;
6868
import org.objectweb.asm.commons.JSRInlinerAdapter;
6969
import org.objectweb.asm.tree.ClassNode;
70+
import org.objectweb.asm.tree.FieldNode;
7071
import org.objectweb.asm.tree.MethodNode;
7172
import org.objectweb.asm.tree.analysis.Analyzer;
7273
import org.objectweb.asm.tree.analysis.AnalyzerException;
@@ -275,6 +276,9 @@ public byte[] transform(
275276
}
276277
ClassNode classNode = parseClassFile(classFilePath, classfileBuffer);
277278
checkMethodParameters(classNode);
279+
if (!checkRecordTypeAnnotation(classNode, definitions, fullyQualifiedClassName)) {
280+
return null;
281+
}
278282
boolean transformed =
279283
performInstrumentation(loader, fullyQualifiedClassName, definitions, classNode);
280284
if (transformed) {
@@ -333,6 +337,37 @@ private void checkMethodParameters(ClassNode classNode) {
333337
}
334338
}
335339

340+
/*
341+
* Because of this bug (https://bugs.openjdk.org/browse/JDK-8376185), when a record using a type
342+
* annotation is retransformed, the internal JVM representation of this record is corrupted
343+
* and lead to exception in best cases but in JVM crashes in worst cases.
344+
* Note: the bug happens only at retransform time and not instrumenting at load time. But the
345+
* fact we have already instrumented the record at load time, will prevent us to remove the
346+
* instrumentation because it needs a retransformation and will lead to corruption of the record
347+
*/
348+
private boolean checkRecordTypeAnnotation(
349+
ClassNode classNode, List<ProbeDefinition> definitions, String fullyQualifiedClassName) {
350+
if (!ASMHelper.isRecord(classNode)) {
351+
return true;
352+
}
353+
if (classNode.fields == null || classNode.fields.isEmpty()) {
354+
return true;
355+
}
356+
for (FieldNode field : classNode.fields) {
357+
if ((field.visibleTypeAnnotations != null && !field.visibleTypeAnnotations.isEmpty())
358+
|| (field.invisibleTypeAnnotations != null
359+
&& !field.invisibleTypeAnnotations.isEmpty())) {
360+
reportInstrumentationFails(
361+
definitions,
362+
fullyQualifiedClassName,
363+
"Instrumentation of a record with type annotation is not supported");
364+
return false;
365+
}
366+
}
367+
// no type annotation for components, not a problem
368+
return true;
369+
}
370+
336371
private boolean skipInstrumentation(ClassLoader loader, String classFilePath) {
337372
if (definitionMatcher.isEmpty()) {
338373
LOGGER.debug("No debugger definitions present.");

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3132,6 +3132,23 @@ public void noInstrumentationForAgentClasses() throws Exception {
31323132
assertNull(result);
31333133
}
31343134

3135+
@Test
3136+
@EnabledForJreRange(min = JRE.JAVA_17)
3137+
public void recordWithTypeAnnotation() throws IOException, URISyntaxException {
3138+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot33";
3139+
LogProbe probe1 = createMethodProbeAtExit(PROBE_ID1, CLASS_NAME, "parse", null);
3140+
TestSnapshotListener listener = installProbes(probe1);
3141+
Class<?> testClass = compileAndLoadClass(CLASS_NAME, "17");
3142+
Reflect.onClass(testClass).call("parse", "1").get();
3143+
ArgumentCaptor<ProbeId> probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class);
3144+
ArgumentCaptor<String> strCaptor = ArgumentCaptor.forClass(String.class);
3145+
verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture());
3146+
assertEquals(PROBE_ID1.getId(), probeIdCaptor.getAllValues().get(0).getId());
3147+
assertEquals(
3148+
"Instrumentation failed for com.datadog.debugger.CapturedSnapshot33: Instrumentation of a record with type annotation is not supported",
3149+
strCaptor.getAllValues().get(0));
3150+
}
3151+
31353152
private TestSnapshotListener setupInstrumentTheWorldTransformer(
31363153
String excludeFileName, String includeFileName) {
31373154
Config config = mock(Config.class);

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,25 @@ public void methodParametersAttributeRecord()
690690
assertEquals(testClass, allValues.get(0));
691691
}
692692

693+
@Test
694+
@EnabledForJreRange(min = JRE.JAVA_17)
695+
public void recordWithTypeAnnotation()
696+
throws IOException, URISyntaxException, UnmodifiableClassException {
697+
// make sure record method are not detected as having methodParameters attribute.
698+
// /!\ record canonical constructor has the MethodParameters attribute,
699+
// but not returned by Class::getDeclaredMethods()
700+
final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot33";
701+
Map<String, byte[]> buffers = compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "17");
702+
Class<?> testClass = loadClass(CLASS_NAME, buffers);
703+
when(inst.getAllLoadedClasses()).thenReturn(new Class[] {testClass});
704+
ConfigurationUpdater configurationUpdater = createConfigUpdater(debuggerSinkWithMockStatusSink);
705+
configurationUpdater.accept(
706+
REMOTE_CONFIG,
707+
singletonList(LogProbe.builder().probeId(PROBE_ID).where(CLASS_NAME, "parse").build()));
708+
verify(inst).getAllLoadedClasses();
709+
verify(inst, times(0)).retransformClasses(any());
710+
}
711+
693712
private DebuggerTransformer createTransformer(
694713
Config tracerConfig,
695714
Configuration configuration,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.datadog.debugger;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@MyTypeAnnotation
9+
public record CapturedSnapshot33(@MyTypeUseAnnotation String strField) {
10+
public static CapturedSnapshot33 parse(String arg) {
11+
return new CapturedSnapshot33(arg);
12+
}
13+
}
14+
15+
16+
@Target({ElementType.TYPE})
17+
@Retention(RetentionPolicy.RUNTIME)
18+
@interface MyTypeAnnotation {
19+
}
20+
21+
@Target({ElementType.TYPE_USE})
22+
@Retention(RetentionPolicy.RUNTIME)
23+
@interface MyTypeUseAnnotation {
24+
}

0 commit comments

Comments
 (0)