Force dynamic typing on AssignReturned annotations (#11884)

This commit is contained in:
Jonas Kunz 2024-08-13 16:30:51 +02:00 committed by GitHub
parent 2100a1646c
commit f74af5476e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 265 additions and 12 deletions

View File

@ -8,6 +8,7 @@ package io.opentelemetry.javaagent.tooling.instrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.tooling.Utils;
import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.ForceDynamicallyTypedAssignReturnedFactory;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
@ -21,7 +22,9 @@ final class TypeTransformerImpl implements TypeTransformer {
this.agentBuilder = agentBuilder;
adviceMapping =
Advice.withCustomMapping()
.with(new Advice.AssignReturned.Factory().withSuppressed(Throwable.class));
.with(
new ForceDynamicallyTypedAssignReturnedFactory(
new Advice.AssignReturned.Factory().withSuppressed(Throwable.class)));
}
@Override

View File

@ -0,0 +1,136 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.annotation.AnnotationValue;
import net.bytebuddy.description.enumeration.EnumerationDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.matcher.ElementMatchers;
/**
* This factory is designed to wrap around {@link Advice.PostProcessor.Factory} and ensures that
* {@link net.bytebuddy.implementation.bytecode.assign.Assigner.Typing#DYNAMIC} is used everywhere.
*
* <p>This helps by avoiding errors where the instrumented bytecode is suddenly unloadable due to
* incompatible assignments and preventing cluttering advice code annotations with the explicit
* typing.
*/
public class ForceDynamicallyTypedAssignReturnedFactory implements Advice.PostProcessor.Factory {
private static final String TO_ARGUMENTS_TYPENAME =
Advice.AssignReturned.ToArguments.class.getName();
private static final String TO_ARGUMENT_TYPENAME =
Advice.AssignReturned.ToArguments.ToArgument.class.getName();
private static final String TO_ALL_ARGUMENTS_TYPENAME =
Advice.AssignReturned.ToAllArguments.class.getName();
private static final String TO_THIS_TYPENAME = Advice.AssignReturned.ToThis.class.getName();
private static final String TO_FIELDS_TYPENAME = Advice.AssignReturned.ToFields.class.getName();
private static final String TO_FIELD_TYPENAME =
Advice.AssignReturned.ToFields.ToField.class.getName();
private static final String TO_RETURNED_TYPENAME =
Advice.AssignReturned.ToReturned.class.getName();
private static final String TO_THROWN_TYPENAME = Advice.AssignReturned.ToThrown.class.getName();
private static final EnumerationDescription DYNAMIC_TYPING =
new EnumerationDescription.ForLoadedEnumeration(Assigner.Typing.DYNAMIC);
private final Advice.PostProcessor.Factory delegate;
public ForceDynamicallyTypedAssignReturnedFactory(Advice.PostProcessor.Factory delegate) {
this.delegate = delegate;
}
@Override
public Advice.PostProcessor make(MethodDescription.InDefinedShape adviceMethod, boolean exit) {
return delegate.make(forceDynamicTyping(adviceMethod), exit);
}
// Visible for testing
static MethodDescription.InDefinedShape forceDynamicTyping(
MethodDescription.InDefinedShape adviceMethod) {
return new MethodDescription.Latent(
adviceMethod.getDeclaringType(),
adviceMethod.getInternalName(),
adviceMethod.getModifiers(),
adviceMethod.getTypeVariables().asTokenList(ElementMatchers.none()),
adviceMethod.getReturnType(),
adviceMethod.getParameters().asTokenList(ElementMatchers.none()),
adviceMethod.getExceptionTypes(),
forceDynamicTyping(adviceMethod.getDeclaredAnnotations()),
adviceMethod.getDefaultValue(),
adviceMethod.getReceiverType());
}
private static List<? extends AnnotationDescription> forceDynamicTyping(
List<? extends AnnotationDescription> declaredAnnotations) {
return declaredAnnotations.stream()
.map(ForceDynamicallyTypedAssignReturnedFactory::forceDynamicTyping)
.collect(Collectors.toList());
}
private static AnnotationDescription forceDynamicTyping(AnnotationDescription anno) {
String name = anno.getAnnotationType().getName();
if (name.equals(TO_FIELD_TYPENAME)
|| name.equals(TO_ARGUMENT_TYPENAME)
|| name.equals(TO_THIS_TYPENAME)
|| name.equals(TO_ALL_ARGUMENTS_TYPENAME)
|| name.equals(TO_RETURNED_TYPENAME)
|| name.equals(TO_THROWN_TYPENAME)) {
return replaceAnnotationValue(
anno, "typing", oldVal -> AnnotationValue.ForEnumerationDescription.of(DYNAMIC_TYPING));
} else if (name.equals(TO_FIELDS_TYPENAME) || name.equals(TO_ARGUMENTS_TYPENAME)) {
return replaceAnnotationValue(
anno,
"value",
oldVal -> {
if (!oldVal.getState().isDefined()) {
return null;
}
AnnotationDescription[] resolve = (AnnotationDescription[]) oldVal.resolve();
if (resolve.length == 0) {
return oldVal;
}
AnnotationDescription[] newValueList =
Arrays.stream(resolve)
.map(ForceDynamicallyTypedAssignReturnedFactory::forceDynamicTyping)
.toArray(AnnotationDescription[]::new);
TypeDescription subType = newValueList[0].getAnnotationType();
return AnnotationValue.ForDescriptionArray.of(subType, newValueList);
});
}
return anno;
}
private static AnnotationDescription replaceAnnotationValue(
AnnotationDescription anno,
String propertyName,
Function<AnnotationValue<?, ?>, AnnotationValue<?, ?>> valueMapper) {
AnnotationValue<?, ?> oldValue = anno.getValue(propertyName);
AnnotationValue<?, ?> newValue = valueMapper.apply(oldValue);
Map<String, AnnotationValue<?, ?>> updatedValues = new HashMap<>();
for (MethodDescription.InDefinedShape property :
anno.getAnnotationType().getDeclaredMethods()) {
AnnotationValue<?, ?> value = anno.getValue(property);
if (!propertyName.equals(property.getName()) && value.getState().isDefined()) {
updatedValues.put(property.getName(), value);
}
}
if (newValue != null) {
updatedValues.put(propertyName, newValue);
}
return new AnnotationDescription.Latent(anno.getAnnotationType(), updatedValues) {};
}
}

View File

@ -30,7 +30,9 @@ public final class IndyTypeTransformerImpl implements TypeTransformer {
this.instrumentationModule = module;
this.adviceMapping =
Advice.withCustomMapping()
.with(new Advice.AssignReturned.Factory().withSuppressed(Throwable.class))
.with(
new ForceDynamicallyTypedAssignReturnedFactory(
new Advice.AssignReturned.Factory().withSuppressed(Throwable.class)))
.bootstrap(
IndyBootstrap.getIndyBootstrapMethod(),
IndyBootstrap.getAdviceBootstrapArguments(instrumentationModule));

View File

@ -0,0 +1,113 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
import static org.assertj.core.api.Assertions.assertThat;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.Advice.AssignReturned;
import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument;
import net.bytebuddy.asm.Advice.AssignReturned.ToFields.ToField;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import org.junit.jupiter.api.Test;
public class ForceDynamicallyTypedAssignReturnedFactoryTest {
@AssignReturned.ToFields(@ToField(value = "foo", index = 42))
@AssignReturned.ToArguments(@ToArgument(value = 3, index = 7))
@AssignReturned.ToReturned(index = 4)
@AssignReturned.ToThrown(index = 5)
@AssignReturned.ToThis(index = 6)
@AssignReturned.ToAllArguments(index = 7)
@Advice.OnMethodEnter
static void testMethod() {}
@Test
public void checkTypingMadeDynamic() {
MethodDescription.InDefinedShape original =
TypeDescription.ForLoadedType.of(ForceDynamicallyTypedAssignReturnedFactoryTest.class)
.getDeclaredMethods()
.stream()
.filter(method -> method.getName().equals("testMethod"))
.findFirst()
.get();
ClassLoader cl = ForceDynamicallyTypedAssignReturnedFactoryTest.class.getClassLoader();
MethodDescription modified =
ForceDynamicallyTypedAssignReturnedFactory.forceDynamicTyping(original);
assertThat(modified.getDeclaredAnnotations())
.hasSize(7)
.anySatisfy(
toFields -> {
assertThat(toFields.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToFields.class.getName());
assertThat((AnnotationDescription[]) toFields.getValue("value").resolve())
.hasSize(1)
.anySatisfy(
toField -> {
assertThat(toField.getValue("value").resolve()).isEqualTo("foo");
assertThat(toField.getValue("index").resolve()).isEqualTo(42);
assertThat(toField.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
});
})
.anySatisfy(
toArguments -> {
assertThat(toArguments.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToArguments.class.getName());
assertThat((AnnotationDescription[]) toArguments.getValue("value").resolve())
.hasSize(1)
.anySatisfy(
toArgument -> {
assertThat(toArgument.getValue("value").resolve()).isEqualTo(3);
assertThat(toArgument.getValue("index").resolve()).isEqualTo(7);
assertThat(toArgument.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
});
})
.anySatisfy(
toReturned -> {
assertThat(toReturned.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToReturned.class.getName());
assertThat(toReturned.getValue("index").resolve()).isEqualTo(4);
assertThat(toReturned.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
})
.anySatisfy(
toThrown -> {
assertThat(toThrown.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToThrown.class.getName());
assertThat(toThrown.getValue("index").resolve()).isEqualTo(5);
assertThat(toThrown.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
})
.anySatisfy(
toThis -> {
assertThat(toThis.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToThis.class.getName());
assertThat(toThis.getValue("index").resolve()).isEqualTo(6);
assertThat(toThis.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
})
.anySatisfy(
toAllArguments -> {
assertThat(toAllArguments.getAnnotationType().getName())
.isEqualTo(AssignReturned.ToAllArguments.class.getName());
assertThat(toAllArguments.getValue("index").resolve()).isEqualTo(7);
assertThat(toAllArguments.getValue("typing").load(cl).resolve())
.isEqualTo(Assigner.Typing.DYNAMIC);
})
.anySatisfy(
onMethodEnter -> {
assertThat(onMethodEnter.getAnnotationType().getName())
.isEqualTo(Advice.OnMethodEnter.class.getName());
});
}
}

View File

@ -5,7 +5,6 @@
package io.opentelemetry.javaagent;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
@ -92,7 +91,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignFieldViaReturnAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToFields(@ToField(value = "privateField", typing = DYNAMIC))
@Advice.AssignReturned.ToFields(@ToField(value = "privateField"))
public static String onEnter(@Advice.Argument(0) String toAssign) {
return toAssign;
}
@ -102,7 +101,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignFieldViaArrayAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToFields(@ToField(value = "privateField", index = 1, typing = DYNAMIC))
@Advice.AssignReturned.ToFields(@ToField(value = "privateField", index = 1))
public static Object[] onEnter(@Advice.Argument(0) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
@ -112,7 +111,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignArgumentViaReturnAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToArguments(@ToArgument(value = 0, typing = DYNAMIC))
@Advice.AssignReturned.ToArguments(@ToArgument(value = 0))
public static String onEnter(@Advice.Argument(1) String toAssign) {
return toAssign;
}
@ -122,7 +121,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignArgumentViaArrayAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToArguments(@ToArgument(value = 0, index = 1, typing = DYNAMIC))
@Advice.AssignReturned.ToArguments(@ToArgument(value = 0, index = 1))
public static Object[] onEnter(@Advice.Argument(1) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
@ -132,7 +131,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignReturnViaReturnAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
public static String onExit(@Advice.Argument(0) String toAssign) {
return toAssign;
}
@ -142,7 +141,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class AssignReturnViaArrayAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned(index = 1, typing = DYNAMIC)
@Advice.AssignReturned.ToReturned(index = 1)
public static Object[] onExit(@Advice.Argument(0) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
@ -152,7 +151,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
public static class GetHelperClassAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(@Advice.Argument(0) boolean localHelper) {
if (localHelper) {
return LocalHelper.class;
@ -177,7 +176,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
throw new RuntimeException("This exception should be suppressed");
}
@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
@Advice.OnMethodExit(
suppress = Throwable.class,
onThrowable = Throwable.class,
@ -194,7 +193,7 @@ public class IndyInstrumentationTestModule extends InstrumentationModule
return new LocalHelper();
}
@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
@Advice.OnMethodExit(
suppress = Throwable.class,
onThrowable = Throwable.class,