Framework: Lock inversion checker

Add an agent-based lock inversion checker. The agent will dynamically
rewrite bytecode to inject calls to LockHook, which runs a checker
on these.

Implement a simple on-thread checker that keeps a per-thread stack
of locks and a global map of lock ordering. As-is, transitivity of
checks is not guaranteed, but should be captured in most practical
cases.

To run a process with the lock checker, start the process with the
agent. The helper script start_with_lockagent.sh can be used for this:

  adb shell setprop wrap.pkg-name script start_with_lockagent

(cherry picked from commit aeb6fce5b33680bc538dbd66979a28bcba1329b4)

Bug: 124744938
Test: manual
Merged-In: Idd9a7066a5b8cb8c0de2e995f08759c98d9473e1
Change-Id: Idd9a7066a5b8cb8c0de2e995f08759c98d9473e1
diff --git a/tools/lock_agent/Android.bp b/tools/lock_agent/Android.bp
new file mode 100644
index 0000000..c54e5b5
--- /dev/null
+++ b/tools/lock_agent/Android.bp
@@ -0,0 +1,61 @@
+cc_library {
+    name: "liblockagent",
+    host_supported: false,
+    srcs: ["agent.cpp"],
+    static_libs: [
+        "libbase_ndk",
+        "slicer_ndk_no_rtti",
+    ],
+    shared_libs: [
+        "libz",  // for slicer (using adler32).
+        "liblog",
+    ],
+    sdk_version: "current",
+    stl: "c++_static",
+    include_dirs: [
+        // NDK headers aren't available in platform NDK builds.
+        "libnativehelper/include_jni",
+    ],
+    header_libs: [
+        "libopenjdkjvmti_headers",
+    ],
+    compile_multilib: "both",
+}
+
+cc_binary_host {
+    name: "lockagenttest",
+    srcs: ["agent.cpp"],
+    static_libs: [
+        "libbase",
+        "libz",
+        "slicer",
+    ],
+    include_dirs: [
+        // NDK headers aren't available in platform NDK builds.
+        "libnativehelper/include_jni",
+    ],
+    header_libs: [
+        "libopenjdkjvmti_headers",
+    ],
+}
+
+java_library {
+    name: "lockagent",
+    srcs: ["java/**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    installable: true,
+}
+
+sh_binary {
+    name: "start_with_lockagent",
+    src: "start_with_lockagent.sh",
+    required: [
+        "liblockagent",
+        "lockagent",
+    ],
+}
diff --git a/tools/lock_agent/agent.cpp b/tools/lock_agent/agent.cpp
new file mode 100644
index 0000000..59bfa2b
--- /dev/null
+++ b/tools/lock_agent/agent.cpp
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include <cstring>
+#include <iostream>
+#include <memory>
+#include <sstream>
+
+#include <jni.h>
+
+#include <jvmti.h>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/unique_fd.h>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+
+// We need dladdr.
+#if !defined(__APPLE__) && !defined(_WIN32)
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#define DEFINED_GNU_SOURCE
+#endif
+#include <dlfcn.h>
+#ifdef DEFINED_GNU_SOURCE
+#undef _GNU_SOURCE
+#undef DEFINED_GNU_SOURCE
+#endif
+#endif
+
+// Slicer's headers have code that triggers these warnings. b/65298177
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#pragma clang diagnostic ignored "-Wsign-compare"
+
+#include <slicer/dex_ir.h>
+#include <slicer/code_ir.h>
+#include <slicer/dex_bytecode.h>
+#include <slicer/dex_ir_builder.h>
+#include <slicer/writer.h>
+#include <slicer/reader.h>
+
+#pragma clang diagnostic pop
+
+namespace {
+
+JavaVM* gJavaVM = nullptr;
+
+// Converts a class name to a type descriptor
+// (ex. "java.lang.String" to "Ljava/lang/String;")
+std::string classNameToDescriptor(const char* className) {
+    std::stringstream ss;
+    ss << "L";
+    for (auto p = className; *p != '\0'; ++p) {
+        ss << (*p == '.' ? '/' : *p);
+    }
+    ss << ";";
+    return ss.str();
+}
+
+using namespace dex;
+using namespace lir;
+
+bool transform(std::shared_ptr<ir::DexFile> dexIr) {
+    bool modified = false;
+
+    std::unique_ptr<ir::Builder> builder;
+
+    for (auto& method : dexIr->encoded_methods) {
+        // Do not look into abstract/bridge/native/synthetic methods.
+        if ((method->access_flags & (kAccAbstract | kAccBridge | kAccNative | kAccSynthetic))
+                != 0) {
+            continue;
+        }
+
+        struct HookVisitor: public Visitor {
+            HookVisitor(std::unique_ptr<ir::Builder>* b, std::shared_ptr<ir::DexFile> d_ir,
+                    CodeIr* c_ir) :
+                    b(b), dIr(d_ir), cIr(c_ir) {
+            }
+
+            bool Visit(Bytecode* bytecode) override {
+                if (bytecode->opcode == OP_MONITOR_ENTER) {
+                    prepare();
+                    addCall(bytecode, OP_INVOKE_STATIC_RANGE, hookType, "preLock", voidType,
+                            objectType, reinterpret_cast<VReg*>(bytecode->operands[0])->reg);
+                    myModified = true;
+                    return true;
+                }
+                if (bytecode->opcode == OP_MONITOR_EXIT) {
+                    prepare();
+                    addCall(bytecode, OP_INVOKE_STATIC_RANGE, hookType, "postLock", voidType,
+                            objectType, reinterpret_cast<VReg*>(bytecode->operands[0])->reg);
+                    myModified = true;
+                    return true;
+                }
+                return false;
+            }
+
+            void prepare() {
+                if (*b == nullptr) {
+                    *b = std::unique_ptr<ir::Builder>(new ir::Builder(dIr));
+                }
+                if (voidType == nullptr) {
+                    voidType = (*b)->GetType("V");
+                    hookType = (*b)->GetType("Lcom/android/lock_checker/LockHook;");
+                    objectType = (*b)->GetType("Ljava/lang/Object;");
+                }
+            }
+
+            void addInst(lir::Instruction* instructionAfter, Opcode opcode,
+                    const std::list<Operand*>& operands) {
+                auto instruction = cIr->Alloc<Bytecode>();
+
+                instruction->opcode = opcode;
+
+                for (auto it = operands.begin(); it != operands.end(); it++) {
+                    instruction->operands.push_back(*it);
+                }
+
+                cIr->instructions.InsertBefore(instructionAfter, instruction);
+            }
+
+            void addCall(lir::Instruction* instructionAfter, Opcode opcode, ir::Type* type,
+                    const char* methodName, ir::Type* returnType,
+                    const std::vector<ir::Type*>& types, const std::list<int>& regs) {
+                auto proto = (*b)->GetProto(returnType, (*b)->GetTypeList(types));
+                auto method = (*b)->GetMethodDecl((*b)->GetAsciiString(methodName), proto, type);
+
+                VRegList* paramRegs = cIr->Alloc<VRegList>();
+                for (auto it = regs.begin(); it != regs.end(); it++) {
+                    paramRegs->registers.push_back(*it);
+                }
+
+                addInst(instructionAfter, opcode,
+                        { paramRegs, cIr->Alloc<Method>(method, method->orig_index) });
+            }
+
+            void addCall(lir::Instruction* instructionAfter, Opcode opcode, ir::Type* type,
+                    const char* methodName, ir::Type* returnType, ir::Type* paramType,
+                    u4 paramVReg) {
+                auto proto = (*b)->GetProto(returnType, (*b)->GetTypeList( { paramType }));
+                auto method = (*b)->GetMethodDecl((*b)->GetAsciiString(methodName), proto, type);
+
+                VRegRange* args = cIr->Alloc<VRegRange>(paramVReg, 1);
+
+                addInst(instructionAfter, opcode,
+                        { args, cIr->Alloc<Method>(method, method->orig_index) });
+            }
+
+            std::unique_ptr<ir::Builder>* b;
+            std::shared_ptr<ir::DexFile> dIr;
+            CodeIr* cIr;
+            ir::Type* voidType = nullptr;
+            ir::Type* hookType = nullptr;
+            ir::Type* objectType = nullptr;
+            bool myModified = false;
+        };
+
+        CodeIr c(method.get(), dexIr);
+        HookVisitor visitor(&builder, dexIr, &c);
+
+        for (auto it = c.instructions.begin(); it != c.instructions.end(); ++it) {
+            lir::Instruction* fi = *it;
+            fi->Accept(&visitor);
+        }
+
+        if (visitor.myModified) {
+            modified = true;
+            c.Assemble();
+        }
+    }
+
+    return modified;
+}
+
+std::pair<dex::u1*, size_t> maybeTransform(const char* name, size_t classDataLen,
+        const unsigned char* classData, dex::Writer::Allocator* allocator) {
+    // Isolate byte code of class class. This is needed as Android usually gives us more
+    // than the class we need.
+    dex::Reader reader(classData, classDataLen);
+
+    dex::u4 index = reader.FindClassIndex(classNameToDescriptor(name).c_str());
+    CHECK_NE(index, kNoIndex);
+    reader.CreateClassIr(index);
+    std::shared_ptr<ir::DexFile> ir = reader.GetIr();
+
+    if (!transform(ir)) {
+        return std::make_pair(nullptr, 0);
+    }
+
+    size_t new_size;
+    dex::Writer writer(ir);
+    dex::u1* newClassData = writer.CreateImage(allocator, &new_size);
+    return std::make_pair(newClassData, new_size);
+}
+
+void transformHook(jvmtiEnv* jvmtiEnv, JNIEnv* env ATTRIBUTE_UNUSED,
+        jclass classBeingRedefined ATTRIBUTE_UNUSED, jobject loader, const char* name,
+        jobject protectionDomain ATTRIBUTE_UNUSED, jint classDataLen,
+        const unsigned char* classData, jint* newClassDataLen, unsigned char** newClassData) {
+    // Even reading the classData array is expensive as the data is only generated when the
+    // memory is touched. Hence call JvmtiAgent#shouldTransform to check if we need to transform
+    // the class.
+
+    // Skip bootclasspath classes. TODO: Make this configurable.
+    if (loader == nullptr) {
+        return;
+    }
+
+    // Do not look into java.* classes. Should technically be filtered by above, but when that's
+    // configurable have this.
+    if (strncmp("java", name, 4) == 0) {
+        return;
+    }
+
+    // Do not look into our Java classes.
+    if (strncmp("com/android/lock_checker", name, 24) == 0) {
+        return;
+    }
+
+    class JvmtiAllocator: public dex::Writer::Allocator {
+    public:
+        explicit JvmtiAllocator(::jvmtiEnv* jvmti) :
+                jvmti_(jvmti) {
+        }
+
+        void* Allocate(size_t size) override {
+            unsigned char* res = nullptr;
+            jvmti_->Allocate(size, &res);
+            return res;
+        }
+
+        void Free(void* ptr) override {
+            jvmti_->Deallocate(reinterpret_cast<unsigned char*>(ptr));
+        }
+
+    private:
+        ::jvmtiEnv* jvmti_;
+    };
+    JvmtiAllocator allocator(jvmtiEnv);
+    std::pair<dex::u1*, size_t> result = maybeTransform(name, classDataLen, classData,
+            &allocator);
+
+    if (result.second > 0) {
+        *newClassData = result.first;
+        *newClassDataLen = static_cast<jint>(result.second);
+    }
+}
+
+void dataDumpRequestHook(jvmtiEnv* jvmtiEnv ATTRIBUTE_UNUSED) {
+    if (gJavaVM == nullptr) {
+        LOG(ERROR) << "No JavaVM for dump";
+        return;
+    }
+    JNIEnv* env;
+    if (gJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOG(ERROR) << "Could not get env for dump";
+        return;
+    }
+    jclass lockHookClass = env->FindClass("com/android/lock_checker/LockHook");
+    if (lockHookClass == nullptr) {
+        env->ExceptionClear();
+        LOG(ERROR) << "Could not find LockHook class";
+        return;
+    }
+    jmethodID dumpId = env->GetStaticMethodID(lockHookClass, "dump", "()V");
+    if (dumpId == nullptr) {
+        env->ExceptionClear();
+        LOG(ERROR) << "Could not find LockHook.dump";
+        return;
+    }
+    env->CallStaticVoidMethod(lockHookClass, dumpId);
+    env->ExceptionClear();
+}
+
+// A function for dladdr to search.
+extern "C" __attribute__ ((visibility ("default"))) void lock_agent_tag_fn() {
+}
+
+bool fileExists(const std::string& path) {
+    struct stat statBuf;
+    int rc = stat(path.c_str(), &statBuf);
+    return rc == 0;
+}
+
+std::string findLockAgentJar() {
+    // Check whether the jar is located next to the agent's so.
+#ifndef __APPLE__
+    {
+        Dl_info info;
+        if (dladdr(reinterpret_cast<const void*>(&lock_agent_tag_fn), /* out */ &info) != 0) {
+            std::string lockAgentSoPath = info.dli_fname;
+            std::string dir = android::base::Dirname(lockAgentSoPath);
+            std::string lockAgentJarPath = dir + "/" + "lockagent.jar";
+            if (fileExists(lockAgentJarPath)) {
+                return lockAgentJarPath;
+            }
+        } else {
+            LOG(ERROR) << "dladdr failed";
+        }
+    }
+#endif
+
+    std::string sysFrameworkPath = "/system/framework/lockagent.jar";
+    if (fileExists(sysFrameworkPath)) {
+        return sysFrameworkPath;
+    }
+
+    std::string relPath = "lockagent.jar";
+    if (fileExists(relPath)) {
+        return relPath;
+    }
+
+    return "";
+}
+
+void prepareHook(jvmtiEnv* env) {
+    // Inject the agent Java code.
+    {
+        std::string path = findLockAgentJar();
+        if (path.empty()) {
+            LOG(FATAL) << "Could not find lockagent.jar";
+        }
+        LOG(INFO) << "Will load Java parts from " << path;
+        jvmtiError res = env->AddToBootstrapClassLoaderSearch(path.c_str());
+        if (res != JVMTI_ERROR_NONE) {
+            LOG(FATAL) << "Could not add lockagent from " << path << " to boot classpath: " << res;
+        }
+    }
+
+    jvmtiCapabilities caps;
+    memset(&caps, 0, sizeof(caps));
+    caps.can_retransform_classes = 1;
+
+    if (env->AddCapabilities(&caps) != JVMTI_ERROR_NONE) {
+        LOG(FATAL) << "Could not add caps";
+    }
+
+    jvmtiEventCallbacks cb;
+    memset(&cb, 0, sizeof(cb));
+    cb.ClassFileLoadHook = transformHook;
+    cb.DataDumpRequest = dataDumpRequestHook;
+
+    if (env->SetEventCallbacks(&cb, sizeof(cb)) != JVMTI_ERROR_NONE) {
+        LOG(FATAL) << "Could not set cb";
+    }
+
+    if (env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr)
+            != JVMTI_ERROR_NONE) {
+        LOG(FATAL) << "Could not enable events";
+    }
+    if (env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_DATA_DUMP_REQUEST, nullptr)
+            != JVMTI_ERROR_NONE) {
+        LOG(FATAL) << "Could not enable events";
+    }
+}
+
+jint attach(JavaVM* vm, char* options ATTRIBUTE_UNUSED, void* reserved ATTRIBUTE_UNUSED) {
+    gJavaVM = vm;
+
+    jvmtiEnv* env;
+    jint jvmError = vm->GetEnv(reinterpret_cast<void**>(&env), JVMTI_VERSION_1_2);
+    if (jvmError != JNI_OK) {
+        return jvmError;
+    }
+
+    prepareHook(env);
+
+    return JVMTI_ERROR_NONE;
+}
+
+extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved) {
+    return attach(vm, options, reserved);
+}
+
+extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
+    return attach(vm, options, reserved);
+}
+
+int locktest_main(int argc, char *argv[]) {
+    if (argc != 3) {
+        LOG(FATAL) << "Need two arguments: dex-file class-name";
+    }
+    struct stat statBuf;
+    int rc = stat(argv[1], &statBuf);
+    if (rc != 0) {
+        PLOG(FATAL) << "Could not get file size for " << argv[1];
+    }
+    std::unique_ptr<char[]> data(new char[statBuf.st_size]);
+    {
+        android::base::unique_fd fd(open(argv[1], O_RDONLY));
+        if (fd.get() == -1) {
+            PLOG(FATAL) << "Could not open file " << argv[1];
+        }
+        if (!android::base::ReadFully(fd.get(), data.get(), statBuf.st_size)) {
+            PLOG(FATAL) << "Could not read file " << argv[1];
+        }
+    }
+
+    class NewDeleteAllocator: public dex::Writer::Allocator {
+    public:
+        explicit NewDeleteAllocator() {
+        }
+
+        void* Allocate(size_t size) override {
+            return new char[size];
+        }
+
+        void Free(void* ptr) override {
+            delete[] reinterpret_cast<char*>(ptr);
+        }
+    };
+    NewDeleteAllocator allocator;
+
+    std::pair<dex::u1*, size_t> result = maybeTransform(argv[2], statBuf.st_size,
+            reinterpret_cast<unsigned char*>(data.get()), &allocator);
+
+    if (result.second == 0) {
+        LOG(INFO) << "No transformation";
+        return 0;
+    }
+
+    std::string newName(argv[1]);
+    newName.append(".new");
+
+    {
+        android::base::unique_fd fd(
+                open(newName.c_str(), O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR));
+        if (fd.get() == -1) {
+            PLOG(FATAL) << "Could not open file " << newName;
+        }
+        if (!android::base::WriteFully(fd.get(), result.first, result.second)) {
+            PLOG(FATAL) << "Could not write file " << newName;
+        }
+    }
+    LOG(INFO) << "Transformed file written to " << newName;
+
+    return 0;
+}
+
+}  // namespace
+
+int main(int argc, char *argv[]) {
+    return locktest_main(argc, argv);
+}
diff --git a/tools/lock_agent/java/com/android/lock_checker/LockHook.java b/tools/lock_agent/java/com/android/lock_checker/LockHook.java
new file mode 100644
index 0000000..95b3181
--- /dev/null
+++ b/tools/lock_agent/java/com/android/lock_checker/LockHook.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2019 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 com.android.lock_checker;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+import android.util.LogWriter;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.StatLogger;
+
+import dalvik.system.AnnotatedStackTraceElement;
+
+import libcore.util.HexEncoding;
+
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Entry class for lock inversion infrastructure. The agent will inject calls to preLock
+ * and postLock, and the hook will call the checker, and store violations.
+ */
+public class LockHook {
+    private static final String TAG = "LockHook";
+
+    private static final Charset sFilenameCharset = Charset.forName("UTF-8");
+
+    private static final HandlerThread sHandlerThread;
+    private static final WtfHandler sHandler;
+
+    private static final AtomicInteger sTotalObtainCount = new AtomicInteger();
+    private static final AtomicInteger sTotalReleaseCount = new AtomicInteger();
+    private static final AtomicInteger sDeepestNest = new AtomicInteger();
+
+    /**
+     * Whether to do the lock check on this thread.
+     */
+    private static final ThreadLocal<Boolean> sDoCheck = ThreadLocal.withInitial(() -> true);
+
+    interface Stats {
+        int ON_THREAD = 0;
+    }
+
+    static final StatLogger sStats = new StatLogger(new String[] { "on-thread", });
+
+    private static final ConcurrentLinkedQueue<Object> sViolations = new ConcurrentLinkedQueue<>();
+    private static final int MAX_VIOLATIONS = 50;
+
+    private static final LockChecker[] sCheckers;
+
+    static {
+        sHandlerThread = new HandlerThread("LockHook:wtf", Process.THREAD_PRIORITY_BACKGROUND);
+        sHandlerThread.start();
+        sHandler = new WtfHandler(sHandlerThread.getLooper());
+
+        sCheckers = new LockChecker[] { new OnThreadLockChecker() };
+    }
+
+    static <T> boolean shouldDumpStacktrace(StacktraceHasher hasher, Map<String, T> dumpedSet,
+            T val, AnnotatedStackTraceElement[] st, int from, int to) {
+        final String stacktraceHash = hasher.stacktraceHash(st, from, to);
+        if (dumpedSet.containsKey(stacktraceHash)) {
+            return false;
+        }
+        dumpedSet.put(stacktraceHash, val);
+        return true;
+    }
+
+    static void updateDeepestNest(int nest) {
+        for (;;) {
+            final int knownDeepest = sDeepestNest.get();
+            if (knownDeepest >= nest) {
+                return;
+            }
+            if (sDeepestNest.compareAndSet(knownDeepest, nest)) {
+                return;
+            }
+        }
+    }
+
+    static void wtf(String message) {
+        sHandler.wtf(message);
+    }
+
+    static void doCheckOnThisThread(boolean check) {
+        sDoCheck.set(check);
+    }
+
+    /**
+     * This method is called when a lock is about to be held. (Except if it's a
+     * synchronized, the lock is already held.)
+     */
+    public static void preLock(Object lock) {
+        if (Thread.currentThread() != sHandlerThread && sDoCheck.get()) {
+            sDoCheck.set(false);
+            try {
+                sTotalObtainCount.incrementAndGet();
+                for (LockChecker checker : sCheckers) {
+                    checker.pre(lock);
+                }
+            } finally {
+                sDoCheck.set(true);
+            }
+        }
+    }
+
+    /**
+     * This method is called when a lock is about to be released.
+     */
+    public static void postLock(Object lock) {
+        if (Thread.currentThread() != sHandlerThread && sDoCheck.get()) {
+            sDoCheck.set(false);
+            try {
+                sTotalReleaseCount.incrementAndGet();
+                for (LockChecker checker : sCheckers) {
+                    checker.post(lock);
+                }
+            } finally {
+                sDoCheck.set(true);
+            }
+        }
+    }
+
+    private static class WtfHandler extends Handler {
+        private static final int MSG_WTF = 1;
+
+        WtfHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void wtf(String msg) {
+            sDoCheck.set(false);
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = msg;
+            obtainMessage(MSG_WTF, args).sendToTarget();
+            sDoCheck.set(true);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_WTF:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    Log.wtf(TAG, (String) args.arg1);
+                    args.recycle();
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Generates a hash for a given stacktrace of a {@link Throwable}.
+     */
+    static class StacktraceHasher {
+        private byte[] mLineNumberBuffer = new byte[4];
+        private final MessageDigest mHash;
+
+        StacktraceHasher() {
+            try {
+                mHash = MessageDigest.getInstance("MD5");
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public String stacktraceHash(Throwable t) {
+            mHash.reset();
+            for (StackTraceElement e : t.getStackTrace()) {
+                hashStackTraceElement(e);
+            }
+            return HexEncoding.encodeToString(mHash.digest());
+        }
+
+        public String stacktraceHash(AnnotatedStackTraceElement[] annotatedStack, int from,
+                int to) {
+            mHash.reset();
+            for (int i = from; i <= to; i++) {
+                hashStackTraceElement(annotatedStack[i].getStackTraceElement());
+            }
+            return HexEncoding.encodeToString(mHash.digest());
+        }
+
+        private void hashStackTraceElement(StackTraceElement e) {
+            if (e.getFileName() != null) {
+                mHash.update(sFilenameCharset.encode(e.getFileName()).array());
+            } else {
+                if (e.getClassName() != null) {
+                    mHash.update(sFilenameCharset.encode(e.getClassName()).array());
+                }
+                if (e.getMethodName() != null) {
+                    mHash.update(sFilenameCharset.encode(e.getMethodName()).array());
+                }
+            }
+
+            final int line = e.getLineNumber();
+            mLineNumberBuffer[0] = (byte) ((line >> 24) & 0xff);
+            mLineNumberBuffer[1] = (byte) ((line >> 16) & 0xff);
+            mLineNumberBuffer[2] = (byte) ((line >> 8) & 0xff);
+            mLineNumberBuffer[3] = (byte) ((line >> 0) & 0xff);
+            mHash.update(mLineNumberBuffer);
+        }
+    }
+
+    static void addViolation(Object o) {
+        sViolations.offer(o);
+        while (sViolations.size() > MAX_VIOLATIONS) {
+            sViolations.poll();
+        }
+    }
+
+    /**
+     * Dump stats to the given PrintWriter.
+     */
+    public static void dump(PrintWriter pw, String indent) {
+        final int oc = LockHook.sTotalObtainCount.get();
+        final int rc = LockHook.sTotalReleaseCount.get();
+        final int dn = LockHook.sDeepestNest.get();
+        pw.print("Lock stats: oc=");
+        pw.print(oc);
+        pw.print(" rc=");
+        pw.print(rc);
+        pw.print(" dn=");
+        pw.print(dn);
+        pw.println();
+
+        for (LockChecker checker : sCheckers) {
+            pw.print(indent);
+            pw.print("  ");
+            checker.dump(pw);
+            pw.println();
+        }
+
+        sStats.dump(pw, indent);
+
+        pw.print(indent);
+        pw.println("Violations:");
+        for (Object v : sViolations) {
+            pw.print(indent); // This won't really indent a multiline string,
+                              // though.
+            pw.println(v);
+        }
+    }
+
+    /**
+     * Dump stats to logcat.
+     */
+    public static void dump() {
+        // Dump to logcat.
+        PrintWriter out = new PrintWriter(new LogWriter(Log.WARN, TAG), true);
+        dump(out, "");
+        out.close();
+    }
+
+    interface LockChecker {
+        void pre(Object lock);
+
+        void post(Object lock);
+
+        int getNumDetected();
+
+        int getNumDetectedUnique();
+
+        String getCheckerName();
+
+        void dump(PrintWriter pw);
+    }
+}
diff --git a/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java b/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java
new file mode 100644
index 0000000..0f3a285
--- /dev/null
+++ b/tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 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 com.android.lock_checker;
+
+import android.util.Log;
+
+import dalvik.system.AnnotatedStackTraceElement;
+import dalvik.system.VMStack;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+class OnThreadLockChecker implements LockHook.LockChecker {
+    private static final String TAG = "LockCheckOnThread";
+
+    private static final boolean SKIP_RECURSIVE = true;
+
+    private final Thread mChecker;
+
+    private final AtomicInteger mNumDetected = new AtomicInteger();
+
+    private final AtomicInteger mNumDetectedUnique = new AtomicInteger();
+
+    // Queue for possible violations, to handle them on the sChecker thread.
+    private final LinkedBlockingQueue<Violation> mQueue = new LinkedBlockingQueue<>();
+
+    // The stack of locks held on the current thread.
+    private final ThreadLocal<List<Object>> mHeldLocks = ThreadLocal
+            .withInitial(() -> new ArrayList<>(10));
+
+    // A cached stacktrace hasher for each thread. The hasher caches internal objects and is not
+    // thread-safe.
+    private final ThreadLocal<LockHook.StacktraceHasher> mStacktraceHasher = ThreadLocal
+            .withInitial(() -> new LockHook.StacktraceHasher());
+
+    // A map of stacktrace hashes we have seen.
+    private final ConcurrentMap<String, Boolean> mDumpedStacktraceHashes =
+            new ConcurrentHashMap<>();
+
+    OnThreadLockChecker() {
+        mChecker = new Thread(() -> checker());
+        mChecker.setName(TAG);
+        mChecker.setPriority(Thread.MIN_PRIORITY);
+        mChecker.start();
+    }
+
+    private static class LockPair {
+        // Consider WeakReference. It will require also caching the String
+        // description for later reporting, though.
+        Object mFirst;
+        Object mSecond;
+
+        private int mCachedHashCode;
+
+        LockPair(Object first, Object second) {
+            mFirst = first;
+            mSecond = second;
+            computeHashCode();
+        }
+
+        public void set(Object newFirst, Object newSecond) {
+            mFirst = newFirst;
+            mSecond = newSecond;
+            computeHashCode();
+        }
+
+        private void computeHashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((mFirst == null) ? 0 : System.identityHashCode(mFirst));
+            result = prime * result + ((mSecond == null) ? 0 : System.identityHashCode(mSecond));
+            mCachedHashCode = result;
+        }
+
+        @Override
+        public int hashCode() {
+            return mCachedHashCode;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            LockPair other = (LockPair) obj;
+            return mFirst == other.mFirst && mSecond == other.mSecond;
+        }
+    }
+
+    private static class OrderData {
+        final int mTid;
+        final String mThreadName;
+        final AnnotatedStackTraceElement[] mStack;
+
+        OrderData(int tid, String threadName, AnnotatedStackTraceElement[] stack) {
+            this.mTid = tid;
+            this.mThreadName = threadName;
+            this.mStack = stack;
+        }
+    }
+
+    private static ConcurrentMap<LockPair, OrderData> sLockOrderMap = new ConcurrentHashMap<>();
+
+    @Override
+    public void pre(Object lock) {
+        handlePre(Thread.currentThread(), lock);
+    }
+
+    @Override
+    public void post(Object lock) {
+        handlePost(Thread.currentThread(), lock);
+    }
+
+    private void handlePre(Thread self, Object lock) {
+        List<Object> heldLocks = mHeldLocks.get();
+
+        LockHook.updateDeepestNest(heldLocks.size() + 1);
+
+        heldLocks.add(lock);
+        if (heldLocks.size() == 1) {
+            return;
+        }
+
+        // Data about this location. Cached and lazily initialized.
+        AnnotatedStackTraceElement[] annotatedStack = null;
+        OrderData orderData = null;
+
+        // Reused tmp pair;
+        LockPair tmp = new LockPair(lock, lock);
+
+        int size = heldLocks.size() - 1;
+        for (int i = 0; i < size; i++) {
+            Object alreadyHeld = heldLocks.get(i);
+            if (SKIP_RECURSIVE && lock == alreadyHeld) {
+                return;
+            }
+
+            // Check if we've already seen alreadyHeld -> lock.
+            tmp.set(alreadyHeld, lock);
+            if (sLockOrderMap.containsKey(tmp)) {
+                continue; // Already seen.
+            }
+
+            // Note: could insert the OrderData now. This would mean we only
+            // report one instance for each order violation, but it avoids
+            // the expensive hashing in handleViolation for duplicate stacks.
+
+            // Locking alreadyHeld -> lock, check whether the inverse exists.
+            tmp.set(lock, alreadyHeld);
+
+            // We technically need a critical section here. Add synchronized and
+            // skip
+            // instrumenting this class. For now, a concurrent hash map is good
+            // enough.
+
+            OrderData oppositeData = sLockOrderMap.getOrDefault(tmp, null);
+            if (oppositeData != null) {
+                if (annotatedStack == null) {
+                    annotatedStack = VMStack.getAnnotatedThreadStackTrace(self);
+                }
+                postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData);
+                continue;
+            }
+
+            // Enter our occurrence.
+            if (annotatedStack == null) {
+                annotatedStack = VMStack.getAnnotatedThreadStackTrace(self);
+            }
+            if (orderData == null) {
+                orderData = new OrderData((int) self.getId(), self.getName(), annotatedStack);
+            }
+            sLockOrderMap.putIfAbsent(new LockPair(alreadyHeld, lock), orderData);
+
+            // Check again whether we might have raced with the opposite.
+            oppositeData = sLockOrderMap.getOrDefault(tmp, null);
+            if (oppositeData != null) {
+                postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData);
+            }
+        }
+    }
+
+    private void handlePost(Thread self, Object lock) {
+        List<Object> heldLocks = mHeldLocks.get();
+        if (heldLocks.isEmpty()) {
+            Log.wtf("LockCheckMine", "Empty thread list on post()");
+            return;
+        }
+        int index = heldLocks.size() - 1;
+        if (heldLocks.get(index) != lock) {
+            Log.wtf("LockCheckMine", "post(" + Violation.describeLock(lock) + ") vs [..., "
+                    + Violation.describeLock(heldLocks.get(index)) + "]");
+            return;
+        }
+        heldLocks.remove(index);
+    }
+
+    private static class Violation {
+        int mSelfTid;
+        String mSelfName;
+        Object mAlreadyHeld;
+        Object mLock;
+        AnnotatedStackTraceElement[] mStack;
+        OrderData mOppositeData;
+
+        Violation(Thread self, Object alreadyHeld, Object lock,
+                AnnotatedStackTraceElement[] stack, OrderData oppositeData) {
+            this.mSelfTid = (int) self.getId();
+            this.mSelfName = self.getName();
+            this.mAlreadyHeld = alreadyHeld;
+            this.mLock = lock;
+            this.mStack = stack;
+            this.mOppositeData = oppositeData;
+        }
+
+        private static String getAnnotatedStackString(AnnotatedStackTraceElement[] stackTrace,
+                int skip, String extra, int prefixAfter, String prefix) {
+            StringBuilder sb = new StringBuilder();
+            for (int i = skip; i < stackTrace.length; i++) {
+                AnnotatedStackTraceElement element = stackTrace[i];
+                sb.append("    ").append(i >= prefixAfter ? prefix : "").append("at ")
+                        .append(element.getStackTraceElement()).append('\n');
+                if (i == skip && extra != null) {
+                    sb.append("    ").append(extra).append('\n');
+                }
+                if (element.getHeldLocks() != null) {
+                    for (Object held : element.getHeldLocks()) {
+                        sb.append("    ").append(i >= prefixAfter ? prefix : "")
+                                .append(describeLocking(held, "locked")).append('\n');
+                    }
+                }
+            }
+            return sb.toString();
+        }
+
+        private static String describeLocking(Object lock, String action) {
+            return String.format("- %s %s", action, describeLock(lock));
+        }
+
+        private static int getTo(AnnotatedStackTraceElement[] stack, Object searchFor) {
+            // Extract the range of the annotated stack.
+            int to = stack.length - 1;
+            for (int i = 0; i < stack.length; i++) {
+                Object[] locks = stack[i].getHeldLocks();
+                if (locks != null) {
+                    for (Object heldLock : locks) {
+                        if (heldLock == searchFor) {
+                            to = i;
+                            break;
+                        }
+                    }
+                }
+            }
+            return to;
+        }
+
+        private static String describeLock(Object lock) {
+            return String.format("<0x%08x> (a %s)", System.identityHashCode(lock),
+                    lock.getClass().getName());
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Lock inversion detected!\n");
+            sb.append("  Locked ");
+            sb.append(describeLock(mLock));
+            sb.append(" -> ");
+            sb.append(describeLock(mAlreadyHeld));
+            sb.append(" on thread ").append(mOppositeData.mTid).append(" (")
+                    .append(mOppositeData.mThreadName).append(")");
+            sb.append(" at:\n");
+            sb.append(getAnnotatedStackString(mOppositeData.mStack, 4,
+                    describeLocking(mAlreadyHeld, "will lock"), getTo(mOppositeData.mStack, mLock)
+                    + 1, "    | "));
+            sb.append("  Locking ");
+            sb.append(describeLock(mAlreadyHeld));
+            sb.append(" -> ");
+            sb.append(describeLock(mLock));
+            sb.append(" on thread ").append(mSelfTid).append(" (").append(mSelfName).append(")");
+            sb.append(" at:\n");
+            sb.append(getAnnotatedStackString(mStack, 4, describeLocking(mLock, "will lock"),
+                    getTo(mStack, mAlreadyHeld) + 1, "    | "));
+
+            return sb.toString();
+        }
+    }
+
+    private void postViolation(Thread self, Object alreadyHeld, Object lock,
+            AnnotatedStackTraceElement[] annotatedStack, OrderData oppositeData) {
+        mQueue.offer(new Violation(self, alreadyHeld, lock, annotatedStack, oppositeData));
+    }
+
+    private void handleViolation(Violation v) {
+        mNumDetected.incrementAndGet();
+        // Extract the range of the annotated stack.
+        int to = Violation.getTo(v.mStack, v.mAlreadyHeld);
+
+        if (LockHook.shouldDumpStacktrace(mStacktraceHasher.get(), mDumpedStacktraceHashes,
+                Boolean.TRUE, v.mStack, 0, to)) {
+            mNumDetectedUnique.incrementAndGet();
+            LockHook.wtf(v.toString());
+            LockHook.addViolation(v);
+        }
+    }
+
+    private void checker() {
+        LockHook.doCheckOnThisThread(false);
+
+        for (;;) {
+            try {
+                Violation v = mQueue.take();
+                handleViolation(v);
+            } catch (InterruptedException e) {
+                // TODO Auto-generated catch block
+                e.printStackTrace();
+            }
+        }
+    }
+
+    @Override
+    public int getNumDetected() {
+        return mNumDetected.get();
+    }
+
+    @Override
+    public int getNumDetectedUnique() {
+        return mNumDetectedUnique.get();
+    }
+
+    @Override
+    public String getCheckerName() {
+        return "Standard LockChecker";
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        pw.print(getCheckerName());
+        pw.print(": d=");
+        pw.print(getNumDetected());
+        pw.print(" du=");
+        pw.print(getNumDetectedUnique());
+    }
+}
diff --git a/tools/lock_agent/start_with_lockagent.sh b/tools/lock_agent/start_with_lockagent.sh
new file mode 100755
index 0000000..9539222
--- /dev/null
+++ b/tools/lock_agent/start_with_lockagent.sh
@@ -0,0 +1,5 @@
+#!/system/bin/sh
+APP=$1
+shift
+$APP -Xplugin:libopenjdkjvmti.so -agentpath:liblockagent.so $@
+