diff options
author | Andreas Gampe <agampe@google.com> | 2019-03-04 14:15:18 -0800 |
---|---|---|
committer | Andreas Gampe <agampe@google.com> | 2019-04-12 10:46:04 -0700 |
commit | 72ede720e26e442f31b96bbd45ae95c1470bf713 (patch) | |
tree | 6ff1a1af2b67c89ed1fdaa00a7f043e7ddff2593 /tools/lock_agent | |
parent | e4f9b348a50f2af162ec800183c5e0c32367176b (diff) |
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
Diffstat (limited to 'tools/lock_agent')
-rw-r--r-- | tools/lock_agent/Android.bp | 61 | ||||
-rw-r--r-- | tools/lock_agent/agent.cpp | 462 | ||||
-rw-r--r-- | tools/lock_agent/java/com/android/lock_checker/LockHook.java | 290 | ||||
-rw-r--r-- | tools/lock_agent/java/com/android/lock_checker/OnThreadLockChecker.java | 368 | ||||
-rwxr-xr-x | tools/lock_agent/start_with_lockagent.sh | 5 |
5 files changed, 1186 insertions, 0 deletions
diff --git a/tools/lock_agent/Android.bp b/tools/lock_agent/Android.bp new file mode 100644 index 000000000000..c54e5b57ccc5 --- /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 000000000000..59bfa2bf849b --- /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 000000000000..95b318101316 --- /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 000000000000..0f3a28598741 --- /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 000000000000..953922230a11 --- /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 $@ + |