blob: 35c75cbbae7c37aa547f6478c5e434fe43580ca1 [file] [log] [blame]
/*
* 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.app.ActivityThread;
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.RuntimeInit;
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<Violation> sViolations =
new ConcurrentLinkedQueue<>();
private static final int MAX_VIOLATIONS = 50;
private static final LockChecker[] sCheckers;
private static boolean sNativeHandling = false;
private static boolean sSimulateCrash = false;
static {
sHandlerThread = new HandlerThread("LockHook:wtf", Process.THREAD_PRIORITY_BACKGROUND);
sHandlerThread.start();
sHandler = new WtfHandler(sHandlerThread.getLooper());
sCheckers = new LockChecker[] { new OnThreadLockChecker() };
sNativeHandling = getNativeHandlingConfig();
sSimulateCrash = getSimulateCrashConfig();
}
private static native boolean getNativeHandlingConfig();
private static native boolean getSimulateCrashConfig();
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(Violation v) {
sHandler.wtf(v);
}
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(Violation v) {
sDoCheck.set(false);
SomeArgs args = SomeArgs.obtain();
args.arg1 = v;
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;
handleViolation((Violation) args.arg1);
args.recycle();
break;
}
}
}
private static void handleViolation(Violation v) {
String msg = v.toString();
Log.wtf(TAG, msg);
if (sNativeHandling) {
nWtf(msg); // Also send to native.
}
if (sSimulateCrash) {
RuntimeInit.logUncaught("LockAgent",
ActivityThread.isSystem() ? "system_server"
: ActivityThread.currentProcessName(),
Process.myPid(), v.getException());
}
}
private static native void nWtf(String msg);
/**
* 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(Violation v) {
wtf(v);
sViolations.offer(v);
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);
}
interface Violation {
Throwable getException();
}
}