Merge "Add a scoped lock injection tool" am: a2170a3d66

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/2470160

Change-Id: Icd4b68954eac0d883a55a1098d3fc7412827c343
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Lee Shombert
2023-03-17 01:41:59 +00:00
committed by Automerger Merge Worker
12 changed files with 624 additions and 52 deletions

View File

@@ -19,3 +19,20 @@ java_binary_host {
"ow2-asm-tree",
],
}
java_library_host {
name: "lockedregioncodeinjection_input",
manifest: "test/manifest.txt",
srcs: ["test/*/*.java"],
static_libs: [
"guava",
"ow2-asm",
"ow2-asm-analysis",
"ow2-asm-commons",
"ow2-asm-tree",
"hamcrest-library",
"hamcrest",
"platform-test-annotations",
"junit",
],
}

View File

@@ -13,37 +13,51 @@
*/
package lockedregioncodeinjection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.TryCatchBlockSorter;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* This visitor does two things:
* This visitor operates on two kinds of targets. For a legacy target, it does the following:
*
* 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre
* 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre
* and post methods calls should it matches one of the given target type in the Configuration.
*
* 2. Find all methods that are synchronized and insert pre method calls in the beginning and post
* method calls just before all return instructions.
*
* For a scoped target, it does the following:
*
* 1. Finds all the MONITOR_ENTER instructions in the byte code. If the target of the opcode is
* named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after
* MONITOR_ENTER opcode completes.
*
* 2. Finds all the MONITOR_EXIT instructions in the byte code. If the target of the opcode is
* named in a --scope switch, then the post method is invoked ON THE TARGET immediately before
* MONITOR_EXIT opcode completes.
*/
class LockFindingClassVisitor extends ClassVisitor {
private String className = null;
@@ -73,12 +87,16 @@ class LockFindingClassVisitor extends ClassVisitor {
class LockFindingMethodVisitor extends MethodVisitor {
private String owner;
private MethodVisitor chain;
private final String className;
private final String methodName;
public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) {
super(Utils.ASM_VERSION, mn);
assert owner != null;
this.owner = owner;
this.chain = chain;
className = owner;
methodName = mn.name;
}
@SuppressWarnings("unchecked")
@@ -93,6 +111,12 @@ class LockFindingClassVisitor extends ClassVisitor {
for (LockTarget t : targets) {
if (t.getTargetDesc().equals("L" + owner + ";")) {
ownerMonitor = t;
if (ownerMonitor.getScoped()) {
final String emsg = String.format(
"scoped targets do not support synchronized methods in %s.%s()",
className, methodName);
throw new RuntimeException(emsg);
}
}
}
}
@@ -118,9 +142,11 @@ class LockFindingClassVisitor extends ClassVisitor {
AbstractInsnNode s = instructions.getFirst();
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false);
insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call);
insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call);
}
boolean anyDup = false;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode s = instructions.get(i);
@@ -131,9 +157,15 @@ class LockFindingClassVisitor extends ClassVisitor {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
LockTarget target = state.getTargets().get(j);
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
target.getPreOwner(), target.getPreMethod(), "()V", false);
insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call);
MethodInsnNode call = methodCall(target, true);
if (target.getScoped()) {
TypeInsnNode cast = typeCast(target);
i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i,
call, cast);
anyDup = true;
} else {
i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
}
}
}
}
@@ -144,8 +176,9 @@ class LockFindingClassVisitor extends ClassVisitor {
if (operand instanceof LockTargetState) {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
// The instruction after a monitor_exit should be a label for the end of the implicit
// catch block that surrounds the synchronized block to call monitor_exit when an exception
// The instruction after a monitor_exit should be a label for
// the end of the implicit catch block that surrounds the
// synchronized block to call monitor_exit when an exception
// occurs.
checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL,
"Expected to find label after monitor exit");
@@ -161,9 +194,16 @@ class LockFindingClassVisitor extends ClassVisitor {
"Expected label to be the end of monitor exit's try block");
LockTarget target = state.getTargets().get(j);
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
target.getPostOwner(), target.getPostMethod(), "()V", false);
insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call);
MethodInsnNode call = methodCall(target, false);
if (target.getScoped()) {
TypeInsnNode cast = typeCast(target);
i += insertInvokeRelease(mn, frameMap, handlersMap, s, i,
call, cast);
anyDup = true;
} else {
insertMethodCallAfter(mn, frameMap, handlersMap, label,
labelIndex, call);
}
}
}
}
@@ -174,16 +214,116 @@ class LockFindingClassVisitor extends ClassVisitor {
MethodInsnNode call =
new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(),
ownerMonitor.getPostMethod(), "()V", false);
insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call);
i++; // Skip ahead. Otherwise, we will revisit this instruction again.
}
}
if (anyDup) {
mn.maxStack++;
}
super.visitEnd();
mn.accept(chain);
}
// Insert a call to a monitor pre handler. The node and the index identify the
// monitorenter call itself. Insert DUP immediately prior to the MONITORENTER.
// Insert the typecast and call (in that order) after the MONITORENTER.
public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call, TypeInsnNode cast) {
InsnList instructions = mn.instructions;
// Insert a DUP right before MONITORENTER, to capture the object being locked.
// Note that the object will be typed as java.lang.Object.
instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
// Insert the call right after the MONITORENTER. These entries are pushed after
// MONITORENTER so they are inserted in reverse order. MONITORENTER should be
// the target of a try/catch block, which means it must be immediately
// followed by a label (which is part of the try/catch block definition).
// Move forward past the label so the invocation in inside the proper block.
// Throw an error if the next instruction is not a label.
node = node.getNext();
if (!(node instanceof LabelNode)) {
throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()",
className, methodName));
}
node = node.getNext();
index = instructions.indexOf(node);
instructions.insertBefore(node, cast);
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
instructions.insertBefore(node, call);
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
return 3;
}
// Insert instructions completely before the current opcode. This is slightly
// different from insertMethodCallBefore(), which inserts the call before MONITOREXIT
// but inserts the start and end labels after MONITOREXIT.
public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call, TypeInsnNode cast) {
InsnList instructions = mn.instructions;
instructions.insertBefore(node, new InsnNode(Opcodes.DUP));
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
instructions.insertBefore(node, cast);
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
instructions.insertBefore(node, call);
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
return 3;
}
}
public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
public static MethodInsnNode methodCall(LockTarget target, boolean pre) {
String spec = "()V";
if (!target.getScoped()) {
if (pre) {
return new MethodInsnNode(
Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec);
} else {
return new MethodInsnNode(
Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec);
}
} else {
if (pre) {
return new MethodInsnNode(
Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec);
} else {
return new MethodInsnNode(
Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec);
}
}
}
public static TypeInsnNode typeCast(LockTarget target) {
if (!target.getScoped()) {
return null;
} else {
// preOwner and postOwner return the same string for scoped targets.
return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner());
}
}
/**
* Insert a method call before the beginning or end of a synchronized method.
*/
public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call) {
List<TryCatchBlockNode> handlers = handlersMap.get(index);
@@ -226,6 +366,22 @@ class LockFindingClassVisitor extends ClassVisitor {
updateCatchHandler(mn, handlers, start, end, handlersMap);
}
// Insert instructions completely before the current opcode. This is slightly different from
// insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the
// start and end labels after MONITOREXIT.
public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call) {
InsnList instructions = mn.instructions;
instructions.insertBefore(node, call);
frameMap.add(index, frameMap.get(index));
handlersMap.add(index, handlersMap.get(index));
return 1;
}
@SuppressWarnings("unchecked")
public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers,
LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) {

View File

@@ -21,14 +21,28 @@ package lockedregioncodeinjection;
public class LockTarget {
public static final LockTarget NO_TARGET = new LockTarget("", null, null);
// The lock which must be instrumented, in Java internal form (L<path>;).
private final String targetDesc;
// The methods to be called when the lock is taken (released). For non-scoped locks,
// these are fully qualified static methods. For scoped locks, these are the
// unqualified names of a member method of the target lock.
private final String pre;
private final String post;
// If true, the pre and post methods are virtual on the target class. The pre and post methods
// are both called while the lock is held. If this field is false then the pre and post methods
// take no parameters and the post method is called after the lock is released. This is legacy
// behavior.
private final boolean scoped;
public LockTarget(String targetDesc, String pre, String post) {
public LockTarget(String targetDesc, String pre, String post, boolean scoped) {
this.targetDesc = targetDesc;
this.pre = pre;
this.post = post;
this.scoped = scoped;
}
public LockTarget(String targetDesc, String pre, String post) {
this(targetDesc, pre, post, false);
}
public String getTargetDesc() {
@@ -40,7 +54,11 @@ public class LockTarget {
}
public String getPreOwner() {
return pre.substring(0, pre.lastIndexOf('.'));
if (scoped) {
return targetDesc.substring(1, targetDesc.length() - 1);
} else {
return pre.substring(0, pre.lastIndexOf('.'));
}
}
public String getPreMethod() {
@@ -52,10 +70,23 @@ public class LockTarget {
}
public String getPostOwner() {
return post.substring(0, post.lastIndexOf('.'));
if (scoped) {
return targetDesc.substring(1, targetDesc.length() - 1);
} else {
return post.substring(0, post.lastIndexOf('.'));
}
}
public String getPostMethod() {
return post.substring(post.lastIndexOf('.') + 1);
}
public boolean getScoped() {
return scoped;
}
@Override
public String toString() {
return targetDesc + ":" + pre + ":" + post;
}
}

View File

@@ -13,10 +13,11 @@
*/
package lockedregioncodeinjection;
import java.util.List;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.analysis.BasicValue;
import java.util.List;
public class LockTargetState extends BasicValue {
private final List<LockTarget> lockTargets;
@@ -31,4 +32,10 @@ public class LockTargetState extends BasicValue {
public List<LockTarget> getTargets() {
return lockTargets;
}
@Override
public String toString() {
return "LockTargetState(" + getType().getDescriptor()
+ ", " + lockTargets.size() + ")";
}
}

View File

@@ -21,7 +21,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
@@ -36,6 +36,7 @@ public class Main {
String legacyTargets = null;
String legacyPreMethods = null;
String legacyPostMethods = null;
List<LockTarget> targets = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("-i".equals(args[i].trim())) {
i++;
@@ -52,23 +53,25 @@ public class Main {
} else if ("--post".equals(args[i].trim())) {
i++;
legacyPostMethods = args[i].trim();
} else if ("--scoped".equals(args[i].trim())) {
i++;
targets.add(Utils.getScopedTarget(args[i].trim()));
}
}
// TODO(acleung): Better help message than asserts.
assert inJar != null;
assert outJar != null;
if (inJar == null) {
throw new RuntimeException("missing input jar path");
}
if (outJar == null) {
throw new RuntimeException("missing output jar path");
}
assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null);
ZipFile zipSrc = new ZipFile(inJar);
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar));
List<LockTarget> targets = null;
if (legacyTargets != null) {
targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
legacyPostMethods);
} else {
targets = Collections.emptyList();
targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
legacyPostMethods));
}
Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries();

View File

@@ -44,4 +44,27 @@ public class Utils {
return config;
}
/**
* Returns a single {@link LockTarget} from a string. The target is a comma-separated list of
* the target class, the request method, the release method, and a boolean which is true if this
* is a scoped target and false if this is a legacy target. The boolean is optional and
* defaults to true.
*/
public static LockTarget getScopedTarget(String arg) {
String[] c = arg.split(",");
if (c.length == 3) {
return new LockTarget(c[0], c[1], c[2], true);
} else if (c.length == 4) {
if (c[3].equals("true")) {
return new LockTarget(c[0], c[1], c[2], true);
} else if (c[3].equals("false")) {
return new LockTarget(c[0], c[1], c[2], false);
} else {
System.err.println("illegal target parameter \"" + c[3] + "\"");
}
}
// Fall through
throw new RuntimeException("invalid scoped target format");
}
}

View File

@@ -17,7 +17,10 @@ import org.junit.Assert;
import org.junit.Test;
/**
* To run the unit tests:
* To run the unit tests, first build the two necessary artifacts. Do this explicitly as they are
* not generally retained by a normal "build all". After lunching a target:
* m lockedregioncodeinjection
* m lockedregioncodeinjection_input
*
* <pre>
* <code>
@@ -29,31 +32,25 @@ import org.junit.Test;
* mkdir -p out
* rm -fr out/*
*
* # Make booster
* javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src&#47;*&#47;*.java -d out/
* pushd out
* jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main *&#47;*.class
* popd
*
* # Make unit tests.
* javac -cp lib/junit-4.12.jar test&#47;*&#47;*.java -d out/
*
* pushd out
* jar cfe test_input.jar lockedregioncodeinjection.Test *&#47;*.class
* popd
* # Paths to the build artifacts. These assume linux-x86; YMMV.
* ROOT=$TOP/out/host/linux-x86
* EXE=$ROOT/bin/lockedregioncodeinjection
* INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar
*
* # Run tool on unit tests.
* java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \
* lockedregioncodeinjection.Main \
* -i out/test_input.jar -o out/test_output.jar \
* $EXE -i $INPUT -o out/test_output.jar \
* --targets 'Llockedregioncodeinjection/TestTarget;' \
* --pre 'lockedregioncodeinjection/TestTarget.boost' \
* --post 'lockedregioncodeinjection/TestTarget.unboost'
*
* # Run unit tests.
* java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \
* java -ea -cp out/test_output.jar \
* org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
* </code>
* OR
* <code>
* bash test/unit-test.sh
* </code>
* </pre>
*/
public class TestMain {
@@ -64,7 +61,9 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
Assert.assertEquals(TestTarget.invokeCount, 0);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
synchronized (t) {
Assert.assertEquals(TestTarget.boostCount, 1);
@@ -75,6 +74,8 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -84,12 +85,16 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
t.synchronizedCall();
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -99,12 +104,16 @@ public class TestMain {
Assert.assertEquals(TestTarget.boostCount, 0);
Assert.assertEquals(TestTarget.unboostCount, 0);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
t.synchronizedCallReturnInt();
Assert.assertEquals(TestTarget.boostCount, 1);
Assert.assertEquals(TestTarget.unboostCount, 1);
Assert.assertEquals(TestTarget.invokeCount, 1);
Assert.assertEquals(TestTarget.boostCountLocked, 0);
Assert.assertEquals(TestTarget.unboostCountLocked, 0);
}
@Test
@@ -253,4 +262,125 @@ public class TestMain {
Assert.assertEquals(TestTarget.invokeCount, 1);
}
@Test
public void testScopedTarget() {
TestScopedTarget target = new TestScopedTarget();
Assert.assertEquals(0, target.scopedLock().mLevel);
synchronized (target.scopedLock()) {
Assert.assertEquals(1, target.scopedLock().mLevel);
}
Assert.assertEquals(0, target.scopedLock().mLevel);
synchronized (target.scopedLock()) {
synchronized (target.scopedLock()) {
Assert.assertEquals(2, target.scopedLock().mLevel);
}
}
Assert.assertEquals(0, target.scopedLock().mLevel);
}
@Test
public void testScopedExceptionHandling() {
TestScopedTarget target = new TestScopedTarget();
Assert.assertEquals(0, target.scopedLock().mLevel);
boolean handled;
// 1: an exception inside the block properly releases the lock.
handled = false;
try {
synchronized (target.scopedLock()) {
Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
Assert.assertEquals(1, target.scopedLock().mLevel);
throw new RuntimeException();
}
} catch (RuntimeException e) {
Assert.assertEquals(0, target.scopedLock().mLevel);
handled = true;
}
Assert.assertEquals(0, target.scopedLock().mLevel);
Assert.assertEquals(true, handled);
// Just verify that the lock can still be taken
Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
// 2: An exception inside the monitor enter function
handled = false;
target.throwOnEnter(true);
try {
synchronized (target.scopedLock()) {
// The exception was thrown inside monitorEnter(), so the code should
// never reach this point.
Assert.assertEquals(0, 1);
}
} catch (RuntimeException e) {
Assert.assertEquals(0, target.scopedLock().mLevel);
handled = true;
}
Assert.assertEquals(0, target.scopedLock().mLevel);
Assert.assertEquals(true, handled);
// Just verify that the lock can still be taken
Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
// 3: An exception inside the monitor exit function
handled = false;
target.throwOnEnter(true);
try {
synchronized (target.scopedLock()) {
Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
Assert.assertEquals(1, target.scopedLock().mLevel);
}
} catch (RuntimeException e) {
Assert.assertEquals(0, target.scopedLock().mLevel);
handled = true;
}
Assert.assertEquals(0, target.scopedLock().mLevel);
Assert.assertEquals(true, handled);
// Just verify that the lock can still be taken
Assert.assertEquals(false, Thread.holdsLock(target.scopedLock()));
}
// Provide an in-class type conversion for the scoped target.
private Object untypedLock(TestScopedTarget target) {
return target.scopedLock();
}
@Test
public void testScopedLockTyping() {
TestScopedTarget target = new TestScopedTarget();
Assert.assertEquals(target.scopedLock().mLevel, 0);
// Scoped lock injection works on the static type of an object. In general, it is
// a very bad idea to do type conversion on scoped locks, but the general rule is
// that conversions within a single method are recognized by the lock injection
// tool and injection occurs. Conversions outside a single method are not
// recognized and injection does not occur.
// 1. Conversion occurs outside the class. The visible type of the lock is Object
// in this block, so no injection takes place on 'untypedLock', even though the
// dynamic type is TestScopedLock.
synchronized (target.untypedLock()) {
Assert.assertEquals(0, target.scopedLock().mLevel);
Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
}
// 2. Conversion occurs inside the class but in another method. The visible type
// of the lock is Object in this block, so no injection takes place on
// 'untypedLock', even though the dynamic type is TestScopedLock.
synchronized (untypedLock(target)) {
Assert.assertEquals(0, target.scopedLock().mLevel);
Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock);
Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
}
// 3. Conversion occurs inside the method. The compiler can determine the type of
// the lock within a single function, so injection does take place here.
Object untypedLock = target.scopedLock();
synchronized (untypedLock) {
Assert.assertEquals(1, target.scopedLock().mLevel);
Assert.assertEquals(true, untypedLock instanceof TestScopedLock);
Assert.assertEquals(true, Thread.holdsLock(target.scopedLock()));
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package lockedregioncodeinjection;
public class TestScopedLock {
public int mEntered = 0;
public int mExited = 0;
public int mLevel = 0;
private final TestScopedTarget mTarget;
TestScopedLock(TestScopedTarget target) {
mTarget = target;
}
void monitorEnter() {
mLevel++;
mEntered++;
mTarget.enter(mLevel);
}
void monitorExit() {
mLevel--;
mExited++;
mTarget.exit(mLevel);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package lockedregioncodeinjection;
public class TestScopedTarget {
public final TestScopedLock mLock;;
private boolean mNextEnterThrows = false;
private boolean mNextExitThrows = false;
TestScopedTarget() {
mLock = new TestScopedLock(this);
}
TestScopedLock scopedLock() {
return mLock;
}
Object untypedLock() {
return mLock;
}
void enter(int level) {
if (mNextEnterThrows) {
mNextEnterThrows = false;
throw new RuntimeException();
}
}
void exit(int level) {
if (mNextExitThrows) {
mNextExitThrows = false;
throw new RuntimeException();
}
}
void throwOnEnter(boolean b) {
mNextEnterThrows = b;
}
void throwOnExit(boolean b) {
mNextExitThrows = b;
}
}

View File

@@ -19,8 +19,17 @@ public class TestTarget {
public static int invokeCount = 0;
public static boolean nextUnboostThrows = false;
// If this is not null, then this is the lock under test. The lock must not be held when boost()
// or unboost() are called.
public static Object mLock = null;
public static int boostCountLocked = 0;
public static int unboostCountLocked = 0;
public static void boost() {
boostCount++;
if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
boostCountLocked++;
}
}
public static void unboost() {
@@ -29,6 +38,9 @@ public class TestTarget {
throw new RuntimeException();
}
unboostCount++;
if (mLock != null && Thread.currentThread().holdsLock(mLock)) {
unboostCountLocked++;
}
}
public static void invoke() {

View File

@@ -0,0 +1 @@
Main-Class: org.junit.runner.JUnitCore

View File

@@ -0,0 +1,98 @@
#! /bin/bash
#
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.
# This script runs the tests for the lockedregioninjectioncode. See
# TestMain.java for the invocation. The script expects that a full build has
# already been done and artifacts are in $TOP/out.
# Compute the default top of the workspace. The following code copies the
# strategy of croot. (croot cannot be usd directly because it is a function and
# functions are not carried over into subshells.) This gives the correct answer
# if run from inside a workspace. If run from outside a workspace, supply TOP
# on the command line.
TOPFILE=build/make/core/envsetup.mk
TOP=$(dirname $(realpath $0))
while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do
TOP=$(dirname $TOP)
done
# TOP is "/" if this script is located outside a workspace.
# If the user supplied a top directory, use it instead
if [[ -n $1 ]]; then
TOP=$1
shift
fi
if [[ -z $TOP || $TOP = / ]]; then
echo "usage: $0 <workspace-root>"
exit 1
elif [[ ! -d $TOP ]]; then
echo "$TOP is not a directory"
exit 1
elif [[ ! -d $TOP/prebuilts/misc/common ]]; then
echo "$TOP does not look like w workspace"
exit 1
fi
echo "Using workspace $TOP"
# Pick up the current java compiler. The lunch target is not very important,
# since most, if not all, will use the same host binaries.
pushd $TOP > /dev/null
. build/envsetup.sh > /dev/null 2>&1
lunch redfin-userdebug > /dev/null 2>&1
popd > /dev/null
# Bail on any error
set -o pipefail
trap 'exit 1' ERR
# Create the two sources
pushd $TOP > /dev/null
m lockedregioncodeinjection
m lockedregioncodeinjection_input
popd > /dev/null
# Create a temporary directory outside of the workspace.
OUT=$TOP/out/host/test/lockedregioncodeinjection
echo
# Clean the directory
if [[ -d $OUT ]]; then rm -r $OUT; fi
mkdir -p $OUT
ROOT=$TOP/out/host/linux-x86
EXE=$ROOT/bin/lockedregioncodeinjection
INP=$ROOT/framework/lockedregioncodeinjection_input.jar
# Run tool on unit tests.
$EXE \
-i $INP -o $OUT/test_output.jar \
--targets 'Llockedregioncodeinjection/TestTarget;' \
--pre 'lockedregioncodeinjection/TestTarget.boost' \
--post 'lockedregioncodeinjection/TestTarget.unboost' \
--scoped 'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit'
# Run unit tests.
java -ea -cp $OUT/test_output.jar \
org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain
# Extract the class files and decompile them for possible post-analysis.
pushd $OUT > /dev/null
jar -x --file test_output.jar lockedregioncodeinjection
for class in lockedregioncodeinjection/*.class; do
javap -c -v $class > ${class%.class}.asm
done
popd > /dev/null
echo "artifacts are in $OUT"