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