/* * 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 sDoCheck = ThreadLocal.withInitial(() -> true); interface Stats { int ON_THREAD = 0; } static final StatLogger sStats = new StatLogger(new String[] { "on-thread", }); private static final ConcurrentLinkedQueue 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 boolean shouldDumpStacktrace(StacktraceHasher hasher, Map 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(); } }