Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package datadog.trace.instrumentation.junit5.execution;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
Expand All @@ -27,6 +28,8 @@
import java.util.List;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.ModifierAdjustment;
import net.bytebuddy.description.modifier.FieldManifestation;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.support.hierarchical.EngineExecutionContext;
import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService;
Expand All @@ -35,7 +38,9 @@

@AutoService(InstrumenterModule.class)
public class JUnit5ExecutionInstrumentation extends InstrumenterModule.CiVisibility
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
implements Instrumenter.ForSingleType,
Instrumenter.HasTypeAdvice,
Instrumenter.HasMethodAdvice {

private final String parentPackageName =
Strings.getPackageName(JUnitPlatformUtils.class.getName());
Expand Down Expand Up @@ -84,6 +89,20 @@ public Reference[] additionalMuzzleReferences() {
return additionalReferences.toArray(new Reference[0]);
}

/**
* Strips the {@code final} modifier from the fields that {@link TestTaskHandle} mutates when
* setting up test retries. Mutating final fields via reflection or method handles is forbidden by
* <a href="https://openjdk.org/jeps/500">JEP 500</a>, while mutating non-final fields remains
* legal. The modifier is stripped when the class is loaded, before any instance is created, so
* the JVM never relies on the fields being final.
*/
@Override
public void typeAdvice(TypeTransformer transformer) {
transformer.applyAdvice(
new ModifierAdjustment()
.withFieldModifiers(namedOneOf("testDescriptor", "node"), FieldManifestation.PLAIN));
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package datadog.trace.instrumentation.junit5.execution;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers;
import datadog.trace.agent.tooling.muzzle.Reference;
import datadog.trace.api.Config;
import net.bytebuddy.asm.ModifierAdjustment;
import net.bytebuddy.description.modifier.FieldManifestation;

/**
* Strips the {@code final} modifier from {@code AbstractTestDescriptor#uniqueId} when the class is
* loaded. {@link TestDescriptorHandle} overwrites this field on cloned descriptors to give each
* test retry a distinct unique ID, and mutating final fields via reflection or method handles is
* forbidden by <a href="https://openjdk.org/jeps/500">JEP 500</a>. Mutating non-final fields
* remains legal, and since the modifier is stripped before any instance of the class is created,
* the JVM never relies on the field being final.
*/
@AutoService(InstrumenterModule.class)
public class JUnit5TestDescriptorInstrumentation extends InstrumenterModule.CiVisibility
implements Instrumenter.ForSingleType, Instrumenter.HasTypeAdvice {

public JUnit5TestDescriptorInstrumentation() {
super("ci-visibility", "junit-5", "test-retry");
}

@Override
public boolean isEnabled() {
return super.isEnabled() && Config.get().isCiVisibilityExecutionPoliciesEnabled();
}

@Override
public String instrumentedType() {
return "org.junit.platform.engine.support.descriptor.AbstractTestDescriptor";
}

@Override
public Reference[] additionalMuzzleReferences() {
return TestDescriptorHandle.MuzzleHelper.compileReferences().toArray(new Reference[0]);
}

@Override
public void typeAdvice(TypeTransformer transformer) {
transformer.applyAdvice(
new ModifierAdjustment()
.withFieldModifiers(NameMatchers.named("uniqueId"), FieldManifestation.PLAIN));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
import java.util.Collections;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.ModifierAdjustment;
import net.bytebuddy.description.modifier.FieldManifestation;

@AutoService(InstrumenterModule.class)
public class KarateExecutionInstrumentation extends InstrumenterModule.CiVisibility
implements Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice {
implements Instrumenter.ForKnownTypes,
Instrumenter.HasTypeAdvice,
Instrumenter.HasMethodAdvice {

public KarateExecutionInstrumentation() {
super("ci-visibility", "karate", "test-retry");
Expand Down Expand Up @@ -58,6 +62,21 @@ public Map<String, String> contextStore() {
"com.intuit.karate.core.Scenario", packageName + ".ExecutionContext");
}

/**
* Strips the {@code final} modifier from {@code ScenarioRuntime#result} when the class is loaded
* ({@code ScenarioResult} has no field with that name, so this is a no-op there). {@link
* KarateUtils#setResult(ScenarioRuntime, ScenarioResult)} overwrites the field after retries so
* that the last execution result is reported, and mutating final fields via reflection or method
* handles is forbidden by <a href="https://openjdk.org/jeps/500">JEP 500</a>. Mutating non-final
* fields remains legal, and since the modifier is stripped before any instance of the class is
* created, the JVM never relies on the field being final.
*/
@Override
public void typeAdvice(TypeTransformer transformer) {
transformer.applyAdvice(
new ModifierAdjustment().withFieldModifiers(named("result"), FieldManifestation.PLAIN));
}

@Override
public void methodAdvice(MethodTransformer transformer) {
// ScenarioRuntime
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package datadog.trace.instrumentation.weaver;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import net.bytebuddy.asm.Advice;
Expand Down Expand Up @@ -36,36 +36,42 @@ public String[] helperClassNames() {
};
}

/**
* The suite event queue is wrapped by replacing the constructor argument before the constructor
* body assigns it to the {@code queue} field. The field is final, so it cannot be overwritten
* after construction: mutating final fields via reflection or method handles is forbidden by <a
* href="https://openjdk.org/jeps/500">JEP 500</a>.
*/
@Override
public void methodAdvice(MethodTransformer transformer) {
// typelevel's implementation (0.9+) uses a LinkedBlockingQueue
transformer.applyAdvice(
isConstructor(), WeaverInstrumentation.class.getName() + "$SbtTaskCreationAdvice");
isConstructor().and(takesArgument(5, named("java.util.concurrent.LinkedBlockingQueue"))),
WeaverInstrumentation.class.getName() + "$LinkedBlockingQueueAdvice");
// disney's implementation (0.8.4+) uses a ConcurrentLinkedQueue
transformer.applyAdvice(
isConstructor().and(takesArgument(5, named("java.util.concurrent.ConcurrentLinkedQueue"))),
WeaverInstrumentation.class.getName() + "$ConcurrentLinkedQueueAdvice");
}

public static class LinkedBlockingQueueAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void wrapQueue(
@Advice.Argument(0) TaskDef taskDef,
@Advice.Argument(value = 5, readOnly = false) LinkedBlockingQueue<SuiteEvent> queue) {
if (!(queue instanceof TaskDefAwareLinkedBlockingQueueProxy)) {
queue = new TaskDefAwareLinkedBlockingQueueProxy<>(taskDef, queue);
}
}
}

public static class SbtTaskCreationAdvice {
// TODO: JEP 500 - avoid mutating final fields
@SuppressForbidden
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onTaskCreation(
@Advice.This Object sbtTask, @Advice.FieldValue("taskDef") TaskDef taskDef) {
try {
Field queueField = sbtTask.getClass().getDeclaredField("queue");
queueField.setAccessible(true);
Object queue = queueField.get(sbtTask);
if (queue instanceof ConcurrentLinkedQueue) {
// disney's implementation (0.8.4+) uses a ConcurrentLinkedQueue for the field
queueField.set(
sbtTask,
new TaskDefAwareConcurrentLinkedQueueProxy<SuiteEvent>(
taskDef, (ConcurrentLinkedQueue<SuiteEvent>) queue));
} else if (queue instanceof LinkedBlockingQueue) {
// typelevel's implementation (0.9+) uses a LinkedBlockingQueue for the field
queueField.set(
sbtTask,
new TaskDefAwareLinkedBlockingQueueProxy<SuiteEvent>(
taskDef, (LinkedBlockingQueue<SuiteEvent>) queue));
}
} catch (Exception ignored) {
public static class ConcurrentLinkedQueueAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void wrapQueue(
@Advice.Argument(0) TaskDef taskDef,
@Advice.Argument(value = 5, readOnly = false) ConcurrentLinkedQueue<SuiteEvent> queue) {
if (!(queue instanceof TaskDefAwareConcurrentLinkedQueueProxy)) {
queue = new TaskDefAwareConcurrentLinkedQueueProxy<>(taskDef, queue);
}
}
}
Expand Down
36 changes: 29 additions & 7 deletions internal-api/src/main/java/datadog/trace/util/UnsafeUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package datadog.trace.util;

import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import org.slf4j.Logger;
Expand Down Expand Up @@ -58,16 +57,39 @@ public static <T> T tryShallowClone(T original) {
}
}

// TODO: JEP 500 - avoid mutating final fields
@SuppressForbidden
private static void cloneFields(Class<?> clazz, Object original, Object clone) throws Exception {
/**
* Copies field values using {@link Unsafe} field offsets instead of core reflection or method
* handles: mutating final fields with those APIs is forbidden by <a
* href="https://openjdk.org/jeps/500">JEP 500</a>, while Unsafe memory access is not affected by
* it (and the clone is a fresh, unpublished instance allocated with {@link
* Unsafe#allocateInstance(Class)}, so the writes are safe).
*/
private static void cloneFields(Class<?> clazz, Object original, Object clone) {
for (Field field : clazz.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) != 0) {
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(original);
field.set(clone, fieldValue);
long offset = UNSAFE.objectFieldOffset(field);
Class<?> type = field.getType();
if (!type.isPrimitive()) {
UNSAFE.putObject(clone, offset, UNSAFE.getObject(original, offset));
} else if (type == int.class) {
UNSAFE.putInt(clone, offset, UNSAFE.getInt(original, offset));
} else if (type == long.class) {
UNSAFE.putLong(clone, offset, UNSAFE.getLong(original, offset));
} else if (type == boolean.class) {
UNSAFE.putBoolean(clone, offset, UNSAFE.getBoolean(original, offset));
} else if (type == byte.class) {
UNSAFE.putByte(clone, offset, UNSAFE.getByte(original, offset));
} else if (type == char.class) {
UNSAFE.putChar(clone, offset, UNSAFE.getChar(original, offset));
} else if (type == short.class) {
UNSAFE.putShort(clone, offset, UNSAFE.getShort(original, offset));
} else if (type == float.class) {
UNSAFE.putFloat(clone, offset, UNSAFE.getFloat(original, offset));
} else if (type == double.class) {
UNSAFE.putDouble(clone, offset, UNSAFE.getDouble(original, offset));
}
}
}
}
Loading