1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.lock_checker; 18 19 import android.util.Log; 20 21 import dalvik.system.AnnotatedStackTraceElement; 22 import dalvik.system.VMStack; 23 24 import java.io.PrintWriter; 25 import java.util.ArrayList; 26 import java.util.List; 27 import java.util.concurrent.ConcurrentHashMap; 28 import java.util.concurrent.ConcurrentMap; 29 import java.util.concurrent.LinkedBlockingQueue; 30 import java.util.concurrent.atomic.AtomicInteger; 31 32 33 class OnThreadLockChecker implements LockHook.LockChecker { 34 private static final String TAG = "LockCheckOnThread"; 35 36 private static final boolean SKIP_RECURSIVE = true; 37 38 private final Thread mChecker; 39 40 private final AtomicInteger mNumDetected = new AtomicInteger(); 41 42 private final AtomicInteger mNumDetectedUnique = new AtomicInteger(); 43 44 // Queue for possible violations, to handle them on the sChecker thread. 45 private final LinkedBlockingQueue<Violation> mQueue = new LinkedBlockingQueue<>(); 46 47 // The stack of locks held on the current thread. 48 private final ThreadLocal<List<Object>> mHeldLocks = ThreadLocal 49 .withInitial(() -> new ArrayList<>(10)); 50 51 // A cached stacktrace hasher for each thread. The hasher caches internal objects and is not 52 // thread-safe. 53 private final ThreadLocal<LockHook.StacktraceHasher> mStacktraceHasher = ThreadLocal 54 .withInitial(() -> new LockHook.StacktraceHasher()); 55 56 // A map of stacktrace hashes we have seen. 57 private final ConcurrentMap<String, Boolean> mDumpedStacktraceHashes = 58 new ConcurrentHashMap<>(); 59 OnThreadLockChecker()60 OnThreadLockChecker() { 61 mChecker = new Thread(() -> checker()); 62 mChecker.setName(TAG); 63 mChecker.setPriority(Thread.MIN_PRIORITY); 64 mChecker.start(); 65 } 66 67 private static class LockPair { 68 // Consider WeakReference. It will require also caching the String 69 // description for later reporting, though. 70 Object mFirst; 71 Object mSecond; 72 73 private int mCachedHashCode; 74 LockPair(Object first, Object second)75 LockPair(Object first, Object second) { 76 mFirst = first; 77 mSecond = second; 78 computeHashCode(); 79 } 80 set(Object newFirst, Object newSecond)81 public void set(Object newFirst, Object newSecond) { 82 mFirst = newFirst; 83 mSecond = newSecond; 84 computeHashCode(); 85 } 86 computeHashCode()87 private void computeHashCode() { 88 final int prime = 31; 89 int result = 1; 90 result = prime * result + ((mFirst == null) ? 0 : System.identityHashCode(mFirst)); 91 result = prime * result + ((mSecond == null) ? 0 : System.identityHashCode(mSecond)); 92 mCachedHashCode = result; 93 } 94 95 @Override hashCode()96 public int hashCode() { 97 return mCachedHashCode; 98 } 99 100 @Override equals(Object obj)101 public boolean equals(Object obj) { 102 if (this == obj) { 103 return true; 104 } 105 if (obj == null) { 106 return false; 107 } 108 if (getClass() != obj.getClass()) { 109 return false; 110 } 111 LockPair other = (LockPair) obj; 112 return mFirst == other.mFirst && mSecond == other.mSecond; 113 } 114 } 115 116 private static class OrderData { 117 final int mTid; 118 final String mThreadName; 119 final AnnotatedStackTraceElement[] mStack; 120 OrderData(int tid, String threadName, AnnotatedStackTraceElement[] stack)121 OrderData(int tid, String threadName, AnnotatedStackTraceElement[] stack) { 122 this.mTid = tid; 123 this.mThreadName = threadName; 124 this.mStack = stack; 125 } 126 } 127 128 private static ConcurrentMap<LockPair, OrderData> sLockOrderMap = new ConcurrentHashMap<>(); 129 130 @Override pre(Object lock)131 public void pre(Object lock) { 132 handlePre(Thread.currentThread(), lock); 133 } 134 135 @Override post(Object lock)136 public void post(Object lock) { 137 handlePost(Thread.currentThread(), lock); 138 } 139 handlePre(Thread self, Object lock)140 private void handlePre(Thread self, Object lock) { 141 List<Object> heldLocks = mHeldLocks.get(); 142 143 LockHook.updateDeepestNest(heldLocks.size() + 1); 144 145 heldLocks.add(lock); 146 if (heldLocks.size() == 1) { 147 return; 148 } 149 150 // Data about this location. Cached and lazily initialized. 151 AnnotatedStackTraceElement[] annotatedStack = null; 152 OrderData orderData = null; 153 154 // Reused tmp pair; 155 LockPair tmp = new LockPair(lock, lock); 156 157 int size = heldLocks.size() - 1; 158 for (int i = 0; i < size; i++) { 159 Object alreadyHeld = heldLocks.get(i); 160 if (SKIP_RECURSIVE && lock == alreadyHeld) { 161 return; 162 } 163 164 // Check if we've already seen alreadyHeld -> lock. 165 tmp.set(alreadyHeld, lock); 166 if (sLockOrderMap.containsKey(tmp)) { 167 continue; // Already seen. 168 } 169 170 // Note: could insert the OrderData now. This would mean we only 171 // report one instance for each order violation, but it avoids 172 // the expensive hashing in handleViolation for duplicate stacks. 173 174 // Locking alreadyHeld -> lock, check whether the inverse exists. 175 tmp.set(lock, alreadyHeld); 176 177 // We technically need a critical section here. Add synchronized and 178 // skip 179 // instrumenting this class. For now, a concurrent hash map is good 180 // enough. 181 182 OrderData oppositeData = sLockOrderMap.getOrDefault(tmp, null); 183 if (oppositeData != null) { 184 if (annotatedStack == null) { 185 annotatedStack = VMStack.getAnnotatedThreadStackTrace(self); 186 } 187 postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData); 188 continue; 189 } 190 191 // Enter our occurrence. 192 if (annotatedStack == null) { 193 annotatedStack = VMStack.getAnnotatedThreadStackTrace(self); 194 } 195 if (orderData == null) { 196 orderData = new OrderData((int) self.getId(), self.getName(), annotatedStack); 197 } 198 sLockOrderMap.putIfAbsent(new LockPair(alreadyHeld, lock), orderData); 199 200 // Check again whether we might have raced with the opposite. 201 oppositeData = sLockOrderMap.getOrDefault(tmp, null); 202 if (oppositeData != null) { 203 postViolation(self, alreadyHeld, lock, annotatedStack, oppositeData); 204 } 205 } 206 } 207 handlePost(Thread self, Object lock)208 private void handlePost(Thread self, Object lock) { 209 List<Object> heldLocks = mHeldLocks.get(); 210 if (heldLocks.isEmpty()) { 211 Log.wtf("LockCheckMine", "Empty thread list on post()"); 212 return; 213 } 214 int index = heldLocks.size() - 1; 215 if (heldLocks.get(index) != lock) { 216 Log.wtf("LockCheckMine", "post(" + Violation.describeLock(lock) + ") vs [..., " 217 + Violation.describeLock(heldLocks.get(index)) + "]"); 218 return; 219 } 220 heldLocks.remove(index); 221 } 222 223 private static class Violation implements LockHook.Violation { 224 int mSelfTid; 225 String mSelfName; 226 Object mAlreadyHeld; 227 Object mLock; 228 AnnotatedStackTraceElement[] mStack; 229 OrderData mOppositeData; 230 231 private static final int STACK_OFFSET = 4; 232 Violation(Thread self, Object alreadyHeld, Object lock, AnnotatedStackTraceElement[] stack, OrderData oppositeData)233 Violation(Thread self, Object alreadyHeld, Object lock, 234 AnnotatedStackTraceElement[] stack, OrderData oppositeData) { 235 this.mSelfTid = (int) self.getId(); 236 this.mSelfName = self.getName(); 237 this.mAlreadyHeld = alreadyHeld; 238 this.mLock = lock; 239 this.mStack = stack; 240 this.mOppositeData = oppositeData; 241 } 242 getAnnotatedStackString(AnnotatedStackTraceElement[] stackTrace, int skip, String extra, int prefixAfter, String prefix)243 private static String getAnnotatedStackString(AnnotatedStackTraceElement[] stackTrace, 244 int skip, String extra, int prefixAfter, String prefix) { 245 StringBuilder sb = new StringBuilder(); 246 for (int i = skip; i < stackTrace.length; i++) { 247 AnnotatedStackTraceElement element = stackTrace[i]; 248 sb.append(" ").append(i >= prefixAfter ? prefix : "").append("at ") 249 .append(element.getStackTraceElement()).append('\n'); 250 if (i == skip && extra != null) { 251 sb.append(" ").append(extra).append('\n'); 252 } 253 if (element.getHeldLocks() != null) { 254 for (Object held : element.getHeldLocks()) { 255 sb.append(" ").append(i >= prefixAfter ? prefix : "") 256 .append(describeLocking(held, "locked")).append('\n'); 257 } 258 } 259 } 260 return sb.toString(); 261 } 262 describeLocking(Object lock, String action)263 private static String describeLocking(Object lock, String action) { 264 return String.format("- %s %s", action, describeLock(lock)); 265 } 266 getTo(AnnotatedStackTraceElement[] stack, Object searchFor)267 private static int getTo(AnnotatedStackTraceElement[] stack, Object searchFor) { 268 // Extract the range of the annotated stack. 269 int to = stack.length - 1; 270 for (int i = 0; i < stack.length; i++) { 271 Object[] locks = stack[i].getHeldLocks(); 272 if (locks != null) { 273 for (Object heldLock : locks) { 274 if (heldLock == searchFor) { 275 to = i; 276 break; 277 } 278 } 279 } 280 } 281 return to; 282 } 283 describeLock(Object lock)284 private static String describeLock(Object lock) { 285 return String.format("<0x%08x> (a %s)", System.identityHashCode(lock), 286 lock.getClass().getName()); 287 } 288 289 // Synthesize an exception. getException()290 public Throwable getException() { 291 RuntimeException inner = new RuntimeException("Previously locked"); 292 inner.setStackTrace(synthesizeStackTrace(mOppositeData.mStack)); 293 294 RuntimeException outer = new RuntimeException(toString(), inner); 295 outer.setStackTrace(synthesizeStackTrace(mStack)); 296 297 return outer; 298 } 299 synthesizeStackTrace(AnnotatedStackTraceElement[] stack)300 private StackTraceElement[] synthesizeStackTrace(AnnotatedStackTraceElement[] stack) { 301 302 StackTraceElement[] out = new StackTraceElement[stack.length - STACK_OFFSET]; 303 for (int i = 0; i < out.length; i++) { 304 out[i] = stack[i + STACK_OFFSET].getStackTraceElement(); 305 } 306 return out; 307 } 308 toString()309 public String toString() { 310 StringBuilder sb = new StringBuilder(); 311 sb.append("Lock inversion detected!\n"); 312 sb.append(" Locked "); 313 sb.append(describeLock(mLock)); 314 sb.append(" -> "); 315 sb.append(describeLock(mAlreadyHeld)); 316 sb.append(" on thread ").append(mOppositeData.mTid).append(" (") 317 .append(mOppositeData.mThreadName).append(")"); 318 sb.append(" at:\n"); 319 sb.append(getAnnotatedStackString(mOppositeData.mStack, STACK_OFFSET, 320 describeLocking(mAlreadyHeld, "will lock"), getTo(mOppositeData.mStack, mLock) 321 + 1, " | ")); 322 sb.append(" Locking "); 323 sb.append(describeLock(mAlreadyHeld)); 324 sb.append(" -> "); 325 sb.append(describeLock(mLock)); 326 sb.append(" on thread ").append(mSelfTid).append(" (").append(mSelfName).append(")"); 327 sb.append(" at:\n"); 328 sb.append(getAnnotatedStackString(mStack, STACK_OFFSET, 329 describeLocking(mLock, "will lock"), 330 getTo(mStack, mAlreadyHeld) + 1, " | ")); 331 332 return sb.toString(); 333 } 334 } 335 postViolation(Thread self, Object alreadyHeld, Object lock, AnnotatedStackTraceElement[] annotatedStack, OrderData oppositeData)336 private void postViolation(Thread self, Object alreadyHeld, Object lock, 337 AnnotatedStackTraceElement[] annotatedStack, OrderData oppositeData) { 338 mQueue.offer(new Violation(self, alreadyHeld, lock, annotatedStack, oppositeData)); 339 } 340 handleViolation(Violation v)341 private void handleViolation(Violation v) { 342 mNumDetected.incrementAndGet(); 343 // Extract the range of the annotated stack. 344 int to = Violation.getTo(v.mStack, v.mAlreadyHeld); 345 346 if (LockHook.shouldDumpStacktrace(mStacktraceHasher.get(), mDumpedStacktraceHashes, 347 Boolean.TRUE, v.mStack, 0, to)) { 348 mNumDetectedUnique.incrementAndGet(); 349 LockHook.addViolation(v); 350 } 351 } 352 checker()353 private void checker() { 354 LockHook.doCheckOnThisThread(false); 355 356 for (;;) { 357 try { 358 Violation v = mQueue.take(); 359 handleViolation(v); 360 } catch (InterruptedException e) { 361 // TODO Auto-generated catch block 362 e.printStackTrace(); 363 } 364 } 365 } 366 367 @Override getNumDetected()368 public int getNumDetected() { 369 return mNumDetected.get(); 370 } 371 372 @Override getNumDetectedUnique()373 public int getNumDetectedUnique() { 374 return mNumDetectedUnique.get(); 375 } 376 377 @Override getCheckerName()378 public String getCheckerName() { 379 return "Standard LockChecker"; 380 } 381 382 @Override dump(PrintWriter pw)383 public void dump(PrintWriter pw) { 384 pw.print(getCheckerName()); 385 pw.print(": d="); 386 pw.print(getNumDetected()); 387 pw.print(" du="); 388 pw.print(getNumDetectedUnique()); 389 } 390 } 391