Skip to content

Commit

Permalink
Force dynamic typing on AssignReturned annotations (#11884)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKunz authored Aug 13, 2024
1 parent 2100a16 commit f74af54
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public IndyTypeTransformerImpl(
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,7 +91,7 @@ public void transform(TypeTransformer transformer) {
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;
}
Expand All @@ -102,7 +101,7 @@ public static String onEnter(@Advice.Argument(0) String toAssign) {
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};
}
Expand All @@ -112,7 +111,7 @@ public static Object[] onEnter(@Advice.Argument(0) String toAssign) {
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;
}
Expand All @@ -122,7 +121,7 @@ public static String onEnter(@Advice.Argument(1) String toAssign) {
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};
}
Expand All @@ -132,7 +131,7 @@ public static Object[] onEnter(@Advice.Argument(1) String toAssign) {
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;
}
Expand All @@ -142,7 +141,7 @@ public static String onExit(@Advice.Argument(0) String toAssign) {
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};
}
Expand All @@ -152,7 +151,7 @@ public static Object[] onExit(@Advice.Argument(0) String toAssign) {
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;
Expand All @@ -177,7 +176,7 @@ public static void onMethodEnter() {
throw new RuntimeException("This exception should be suppressed");
}

@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
@Advice.OnMethodExit(
suppress = Throwable.class,
onThrowable = Throwable.class,
Expand All @@ -194,7 +193,7 @@ public static LocalHelper onMethodEnter() {
return new LocalHelper();
}

@Advice.AssignReturned.ToReturned(typing = DYNAMIC)
@Advice.AssignReturned.ToReturned
@Advice.OnMethodExit(
suppress = Throwable.class,
onThrowable = Throwable.class,
Expand Down

0 comments on commit f74af54

Please sign in to comment.