Skip to content

Commit 3745249

Browse files
authored
Prevent retransforming record with type annotation (#10824)
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 fix forbidden api Co-authored-by: jean-philippe.bempel <jean-philippe.bempel@datadoghq.com>
1 parent 3c59e03 commit 3745249

File tree

5 files changed

+185
-4
lines changed

5 files changed

+185
-4
lines changed

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

Lines changed: 90 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,30 @@
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", true, null);
67+
getRecordComponentsMethod = recordClass.getClass().getDeclaredMethod("getRecordComponents");
68+
Class<?> recordComponentClass =
69+
Class.forName("java.lang.reflect.RecordComponent", true, null);
70+
getAnnotatedTypesMethod = recordComponentClass.getDeclaredMethod("getAnnotatedType");
71+
} catch (Exception e) {
72+
LOGGER.debug("Exception initializing reflection constants", e);
73+
}
74+
}
75+
GET_RECORD_COMPONENTS_METHOD = getRecordComponentsMethod;
76+
GET_ANNOTATED_TYPES_METHOD = getAnnotatedTypesMethod;
77+
}
4978

5079
public interface TransformerSupplier {
5180
DebuggerTransformer supply(
@@ -56,9 +85,6 @@ DebuggerTransformer supply(
5685
DebuggerSink debuggerSink);
5786
}
5887

59-
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUpdater.class);
60-
private static final int MINUTES_BETWEEN_ERROR_LOG = 5;
61-
6288
private final Instrumentation instrumentation;
6389
private final TransformerSupplier transformerSupplier;
6490
private final Lock configurationLock = new ReentrantLock();
@@ -185,6 +211,7 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne
185211
List<Class<?>> changedClasses =
186212
finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes);
187213
changedClasses = detectMethodParameters(changes, changedClasses);
214+
changedClasses = detectRecordWithTypeAnnotation(changes, changedClasses);
188215
retransformClasses(changedClasses);
189216
// ensures that we have at least re-transformed 1 class
190217
if (changedClasses.size() > 0) {
@@ -248,6 +275,65 @@ private List<Class<?>> detectMethodParameters(
248275
return result;
249276
}
250277

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