Merge "Framework: Lock inversion checker"

This commit is contained in:
Andreas Gampe
2019-04-12 22:17:15 +00:00
committed by Gerrit Code Review
5 changed files with 1186 additions and 0 deletions

View File

@@ -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",
],
}

462
tools/lock_agent/agent.cpp Normal file
View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,5 @@
#!/system/bin/sh
APP=$1
shift
$APP -Xplugin:libopenjdkjvmti.so -agentpath:liblockagent.so $@