Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 00394fc0 authored by Lee Shombert's avatar Lee Shombert
Browse files

Add a scoped lock injection tool

Bug: 271576290

The lock injection tool now handles two types of lock injection:
legacy and "scoped".  The legacy behavior is changed: the request
method used to be called immediately after the lock is taken.  The new
behavior is that the request method is called immediately before the
lock is taken.

Legacy behavior is changed, and is as follows:
  1. The request method is called immediately before the lock is
     taken.
  2. The release method is called immediately after the lock is
     released.  The release method is NOT holding the lock when it is
     called.
  3. The request and release methods are static and the signature of
     the functions is "()V".
  4. Synchronized methods and blocks are supported.

Scoped behavior is new, and is as follows:
  1. The request method is called immediately after the lock is taken.
  2. The release method is called immediately before the lock is
     released.
  3. Both request and release methods are instance methods of the
     traced object and are called on the traced object (the target of
     synchronized()).  The signature of the methods is "()V".
  4. Synchronized blocks are supported. Synchronized methods are not
     supported.

Scoped behavior is invoked on a data type with the new --scoped
switch.  The format of the switch is:
  --scoped <target-class>,<request-method>,<release-method>,<scoped>

The <scoped> argument to the --scoped switch selects legacy (false) or
scoped (true) behavior.  The legacy command line arguments only select
legacy behavior.

The Android.bp file has been updated to create test artifacts.  The
test procedure described in TestMain.java has been updated.  In
addition, the unit test for this feature has been wrapped in a script.
New tests have been added for the scoped lock.

Test: the following:
 * run the lockedregioncodeinjection unit test
 * build an image and verify that the java byte code is properly
   updated for the new lock injection
 * build a complete Android image and verify no regressions

Change-Id: I4fbfbf8873c385005cbf67a5b02aff204d145030
parent f2c06577
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -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",
    ],
}
+176 −20
Original line number Diff line number Diff line
@@ -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));

    public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
            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 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) {
+34 −3
Original line number Diff line number Diff line
@@ -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,8 +54,12 @@ public class LockTarget {
    }

    public String getPreOwner() {
        if (scoped) {
            return targetDesc.substring(1, targetDesc.length() - 1);
        } else {
            return pre.substring(0, pre.lastIndexOf('.'));
        }
    }

    public String getPreMethod() {
        return pre.substring(pre.lastIndexOf('.') + 1);
@@ -52,10 +70,23 @@ public class LockTarget {
    }

    public String getPostOwner() {
        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;
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -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() + ")";
    }
}
+13 −10
Original line number Diff line number Diff line
@@ -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();
Loading