• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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.google.android.lint
18 
19 import com.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Context
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Implementation
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.Location
27 import com.android.tools.lint.detector.api.Scope
28 import com.android.tools.lint.detector.api.Severity
29 import com.android.tools.lint.detector.api.SourceCodeScanner
30 import com.intellij.psi.search.PsiSearchScopeUtil
31 import com.intellij.psi.search.SearchScope
32 import org.jetbrains.uast.UBlockExpression
33 import org.jetbrains.uast.UCallExpression
34 import org.jetbrains.uast.UDeclarationsExpression
35 import org.jetbrains.uast.UElement
36 import org.jetbrains.uast.ULocalVariable
37 import org.jetbrains.uast.USimpleNameReferenceExpression
38 import org.jetbrains.uast.UTryExpression
39 import org.jetbrains.uast.getParentOfType
40 import org.jetbrains.uast.getQualifiedParentOrThis
41 import org.jetbrains.uast.getUCallExpression
42 import org.jetbrains.uast.skipParenthesizedExprDown
43 import org.jetbrains.uast.skipParenthesizedExprUp
44 
45 /**
46  * Lint Detector that finds issues with improper usages of the token returned by
47  * Binder.clearCallingIdentity()
48  */
49 @Suppress("UnstableApiUsage")
50 class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
51     /** Map of <Token variable name, Token object> */
52     private val tokensMap = mutableMapOf<String, Token>()
53 
54     override fun getApplicableUastTypes(): List<Class<out UElement?>> =
55             listOf(ULocalVariable::class.java, UCallExpression::class.java)
56 
57     override fun createUastHandler(context: JavaContext): UElementHandler =
58             TokenUastHandler(context)
59 
60     /** File analysis starts with a clear map */
61     override fun beforeCheckFile(context: Context) {
62         tokensMap.clear()
63     }
64 
65     /**
66      * - If tokensMap has tokens after checking the file -> reports all locations as unused token
67      * issue incidents
68      * - File analysis ends with a clear map
69      */
70     override fun afterCheckFile(context: Context) {
71         for (token in tokensMap.values) {
72             context.report(
73                     ISSUE_UNUSED_TOKEN,
74                     token.location,
75                     getIncidentMessageUnusedToken(token.variableName)
76             )
77         }
78         tokensMap.clear()
79     }
80 
81     /** UAST handler that analyses elements and reports incidents */
82     private inner class TokenUastHandler(val context: JavaContext) : UElementHandler() {
83         /**
84          * For every variable initialization with Binder.clearCallingIdentity():
85          * - Checks for non-final token issue
86          * - Checks for unused token issue within different scopes
87          * - Checks for nested calls of clearCallingIdentity() issue
88          * - Checks for clearCallingIdentity() not followed by try-finally issue
89          * - Stores token variable name, scope in the file, location and finally block in tokensMap
90          */
91         override fun visitLocalVariable(node: ULocalVariable) {
92             val initializer = node.uastInitializer?.skipParenthesizedExprDown()
93             val rhsExpression = initializer?.getUCallExpression() ?: return
94             if (!isMethodCall(rhsExpression, Method.BINDER_CLEAR_CALLING_IDENTITY)) return
95             val location = context.getLocation(node as UElement)
96             val variableName = node.getName()
97             if (!node.isFinal) {
98                 context.report(
99                         ISSUE_NON_FINAL_TOKEN,
100                         location,
101                         getIncidentMessageNonFinalToken(variableName)
102                 )
103             }
104             // If there exists an unused variable with the same name in the map, we can imply that
105             // we left the scope of the previous declaration, so we need to report the unused token
106             val oldToken = tokensMap[variableName]
107             if (oldToken != null) {
108                 context.report(
109                         ISSUE_UNUSED_TOKEN,
110                         oldToken.location,
111                         getIncidentMessageUnusedToken(oldToken.variableName)
112                 )
113             }
114             // If there exists a token in the same scope as the current new token, it means that
115             // clearCallingIdentity() has been called at least twice without immediate restoration
116             // of identity, so we need to report the nested call of clearCallingIdentity()
117             val firstCallToken = findFirstTokenInScope(node)
118             if (firstCallToken != null) {
119                 context.report(
120                         ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
121                         createNestedLocation(firstCallToken, location),
122                         getIncidentMessageNestedClearIdentityCallsPrimary(
123                                 firstCallToken.variableName,
124                                 variableName
125                         )
126                 )
127             }
128             // If the next statement in the tree is not a try-finally statement, we need to report
129             // the "clearCallingIdentity() is not followed by try-finally" issue
130             val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression)
131                     ?.finallyClause
132             if (finallyClause == null) {
133                 context.report(
134                         ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
135                         location,
136                         getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName)
137                 )
138             }
139             tokensMap[variableName] = Token(
140                     variableName,
141                     node.sourcePsi?.getUseScope(),
142                     location,
143                     finallyClause
144             )
145         }
146 
147         /**
148          * For every method():
149          * - Checks use of caller-aware methods issue
150          * For every call of Binder.restoreCallingIdentity(token):
151          * - Checks for restoreCallingIdentity() not in the finally block issue
152          * - Removes token from tokensMap if token is within the scope of the method
153          */
154         override fun visitCallExpression(node: UCallExpression) {
155             val token = findFirstTokenInScope(node)
156             if (isCallerAwareMethod(node) && token != null) {
157                 context.report(
158                         ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
159                         context.getLocation(node),
160                         getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
161                                 token.variableName,
162                                 node.asRenderString()
163                         )
164                 )
165                 return
166             }
167             if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return
168             val first = node.valueArguments[0].skipParenthesizedExprDown()
169             val arg = first as? USimpleNameReferenceExpression ?: return
170             val variableName = arg.identifier
171             val originalScope = tokensMap[variableName]?.scope ?: return
172             val psi = arg.sourcePsi ?: return
173             // Checks if Binder.restoreCallingIdentity(token) is called within the scope of the
174             // token declaration. If not within the scope, no action is needed because the token is
175             // irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
176             if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
177             // - We do not report "restore identity call not in finally" issue when there is no
178             // finally block because that case is already handled by "clear identity call not
179             // followed by try-finally" issue
180             // - UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
181             // receiver.selector, so to get the call's immediate parent we need to get the topmost
182             // parent qualified reference expression and access its parent
183             if (tokensMap[variableName]?.finallyBlock != null &&
184                     skipParenthesizedExprUp(node.getQualifiedParentOrThis().uastParent) !=
185                         tokensMap[variableName]?.finallyBlock) {
186                 context.report(
187                         ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
188                         context.getLocation(node),
189                         getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName)
190                 )
191             }
192             tokensMap.remove(variableName)
193         }
194 
195         private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
196                 callerAwareMethods.any { method -> isMethodCall(expression, method) }
197 
198         private fun isMethodCall(
199             expression: UCallExpression,
200             method: Method
201         ): Boolean {
202             val psiMethod = expression.resolve() ?: return false
203             return psiMethod.getName() == method.methodName &&
204                     context.evaluator.methodMatches(
205                             psiMethod,
206                             method.className,
207                             /* allowInherit */ true,
208                             *method.args
209                     )
210         }
211 
212         /**
213          * ULocalVariable in the file tree:
214          *
215          * UBlockExpression
216          *     UDeclarationsExpression
217          *         ULocalVariable
218          *         ULocalVariable
219          *     UTryStatement
220          *     etc.
221          *
222          * To get the next statement of ULocalVariable:
223          * - If there exists a next sibling in UDeclarationsExpression, return the sibling
224          * - If there exists a next sibling of UDeclarationsExpression in UBlockExpression, return
225          *   the sibling
226          * - Otherwise, return null
227          *
228          * Example 1 - the next sibling is in UDeclarationsExpression:
229          * Code:
230          * {
231          *     int num1 = 0, num2 = methodThatThrowsException();
232          * }
233          * Returns: num2 = methodThatThrowsException()
234          *
235          * Example 2 - the next sibling is in UBlockExpression:
236          * Code:
237          * {
238          *     int num1 = 0;
239          *     methodThatThrowsException();
240          * }
241          * Returns: methodThatThrowsException()
242          *
243          * Example 3 - no next sibling;
244          * Code:
245          * {
246          *     int num1 = 0;
247          * }
248          * Returns: null
249          */
250         private fun getNextStatementOfLocalVariable(node: ULocalVariable): UElement? {
251             val declarationsExpression = node.uastParent as? UDeclarationsExpression ?: return null
252             val declarations = declarationsExpression.declarations
253             val indexInDeclarations = declarations.indexOf(node)
254             if (indexInDeclarations != -1 && declarations.size > indexInDeclarations + 1) {
255                 return declarations[indexInDeclarations + 1]
256             }
257             val enclosingBlock = node
258                     .getParentOfType<UBlockExpression>(strict = true) ?: return null
259             val expressions = enclosingBlock.expressions
260             val indexInBlock = expressions.indexOf(declarationsExpression as UElement)
261             return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1)
262         }
263     }
264 
265     private fun findFirstTokenInScope(node: UElement): Token? {
266         val psi = node.sourcePsi ?: return null
267         for (token in tokensMap.values) {
268             if (token.scope != null && PsiSearchScopeUtil.isInScope(token.scope, psi)) {
269                 return token
270             }
271         }
272         return null
273     }
274 
275     /**
276      * Creates a new instance of the primary location with the secondary location
277      *
278      * Here, secondary location is the helper location that shows where the issue originated
279      *
280      * The detector reports locations as objects, so when we add a secondary location to a location
281      * that has multiple issues, the secondary location gets displayed every time a location is
282      * referenced.
283      *
284      * Example:
285      * 1: final long token1 = Binder.clearCallingIdentity();
286      * 2: long token2 = Binder.clearCallingIdentity();
287      * 3: Binder.restoreCallingIdentity(token1);
288      * 4: Binder.restoreCallingIdentity(token2);
289      *
290      * Explanation:
291      * token2 has 2 issues: NonFinal and NestedCalls
292      *
293      *     Lint report without cloning                        Lint report with cloning
294      * line 2: [NonFinalIssue]                            line 2: [NonFinalIssue]
295      *     line 1: [NestedCallsIssue]
296      * line 2: [NestedCallsIssue]                            line 2: [NestedCallsIssue]
297      *     line 1: [NestedCallsIssue]                           line 1: [NestedCallsIssue]
298      */
299     private fun createNestedLocation(
300         firstCallToken: Token,
301         secondCallTokenLocation: Location
302     ): Location {
303         return cloneLocation(secondCallTokenLocation)
304                 .withSecondary(
305                         cloneLocation(firstCallToken.location),
306                         getIncidentMessageNestedClearIdentityCallsSecondary(
307                                 firstCallToken.variableName
308                         )
309                 )
310     }
311 
312     private fun cloneLocation(location: Location): Location {
313         // smart cast of location.start to 'Position' is impossible, because 'location.start' is a
314         // public API property declared in different module
315         val locationStart = location.start
316         return if (locationStart == null) {
317             Location.create(location.file)
318         } else {
319             Location.create(location.file, locationStart, location.end)
320         }
321     }
322 
323     private enum class Method(
324         val className: String,
325         val methodName: String,
326         val args: Array<String>
327     ) {
328         BINDER_CLEAR_CALLING_IDENTITY(CLASS_BINDER, "clearCallingIdentity", emptyArray()),
329         BINDER_RESTORE_CALLING_IDENTITY(CLASS_BINDER, "restoreCallingIdentity", arrayOf("long")),
330         BINDER_GET_CALLING_PID(CLASS_BINDER, "getCallingPid", emptyArray()),
331         BINDER_GET_CALLING_UID(CLASS_BINDER, "getCallingUid", emptyArray()),
332         BINDER_GET_CALLING_UID_OR_THROW(CLASS_BINDER, "getCallingUidOrThrow", emptyArray()),
333         BINDER_GET_CALLING_USER_HANDLE(CLASS_BINDER, "getCallingUserHandle", emptyArray()),
334         USER_HANDLE_GET_CALLING_APP_ID(CLASS_USER_HANDLE, "getCallingAppId", emptyArray()),
335         USER_HANDLE_GET_CALLING_USER_ID(CLASS_USER_HANDLE, "getCallingUserId", emptyArray())
336     }
337 
338     private data class Token(
339         val variableName: String,
340         val scope: SearchScope?,
341         val location: Location,
342         val finallyBlock: UElement?
343     )
344 
345     companion object {
346         const val CLASS_BINDER = "android.os.Binder"
347         const val CLASS_USER_HANDLE = "android.os.UserHandle"
348 
349         private val callerAwareMethods = listOf(
350                 Method.BINDER_GET_CALLING_PID,
351                 Method.BINDER_GET_CALLING_UID,
352                 Method.BINDER_GET_CALLING_UID_OR_THROW,
353                 Method.BINDER_GET_CALLING_USER_HANDLE,
354                 Method.USER_HANDLE_GET_CALLING_APP_ID,
355                 Method.USER_HANDLE_GET_CALLING_USER_ID
356         )
357 
358         /** Issue: unused token from Binder.clearCallingIdentity() */
359         @JvmField
360         val ISSUE_UNUSED_TOKEN: Issue = Issue.create(
361                 id = "UnusedTokenOfOriginalCallingIdentity",
362                 briefDescription = "Unused token of Binder.clearCallingIdentity()",
363                 explanation = """
364                     You cleared the original calling identity with \
365                     `Binder.clearCallingIdentity()`, but have not used the returned token to \
366                     restore the identity.
367 
368                     Call `Binder.restoreCallingIdentity(token)` in the `finally` block, at the end \
369                     of the method or when you need to restore the identity.
370 
371                     `token` is the result of `Binder.clearCallingIdentity()`
372                     """,
373                 category = Category.SECURITY,
374                 priority = 6,
375                 severity = Severity.WARNING,
376                 implementation = Implementation(
377                         CallingIdentityTokenDetector::class.java,
378                         Scope.JAVA_FILE_SCOPE
379                 )
380         )
381 
382         private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " +
383                 "not been used to restore the calling identity. Introduce a `try`-`finally` " +
384                 "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " +
385                 "in `finally` or remove `$variableName`."
386 
387         /** Issue: non-final token from Binder.clearCallingIdentity() */
388         @JvmField
389         val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create(
390                 id = "NonFinalTokenOfOriginalCallingIdentity",
391                 briefDescription = "Non-final token of Binder.clearCallingIdentity()",
392                 explanation = """
393                     You cleared the original calling identity with \
394                     `Binder.clearCallingIdentity()`, but have not made the returned token `final`.
395 
396                     The token should be `final` in order to prevent it from being overwritten, \
397                     which can cause problems when restoring the identity with \
398                     `Binder.restoreCallingIdentity(token)`.
399                     """,
400                 category = Category.SECURITY,
401                 priority = 6,
402                 severity = Severity.WARNING,
403                 implementation = Implementation(
404                         CallingIdentityTokenDetector::class.java,
405                         Scope.JAVA_FILE_SCOPE
406                 )
407         )
408 
409         private fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is " +
410                 "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " +
411                 "`$variableName`."
412 
413         /** Issue: nested calls of Binder.clearCallingIdentity() */
414         @JvmField
415         val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create(
416                 id = "NestedClearCallingIdentityCalls",
417                 briefDescription = "Nested calls of Binder.clearCallingIdentity()",
418                 explanation = """
419                     You cleared the original calling identity with \
420                     `Binder.clearCallingIdentity()` twice without restoring identity with the \
421                     result of the first call.
422 
423                     Make sure to restore the identity after each clear identity call.
424                     """,
425                 category = Category.SECURITY,
426                 priority = 6,
427                 severity = Severity.WARNING,
428                 implementation = Implementation(
429                         CallingIdentityTokenDetector::class.java,
430                         Scope.JAVA_FILE_SCOPE
431                 )
432         )
433 
434         private fun getIncidentMessageNestedClearIdentityCallsPrimary(
435             firstCallVariableName: String,
436             secondCallVariableName: String
437         ): String = "The calling identity has already been cleared and returned into " +
438                 "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " +
439                 "restoring the calling identity with " +
440                 "`Binder.restoreCallingIdentity($firstCallVariableName)`."
441 
442         private fun getIncidentMessageNestedClearIdentityCallsSecondary(
443             firstCallVariableName: String
444         ): String = "Location of the `$firstCallVariableName` declaration."
445 
446         /** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */
447         @JvmField
448         val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create(
449                 id = "ClearIdentityCallNotFollowedByTryFinally",
450                 briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " +
451                         "statement",
452                 explanation = """
453                     You cleared the original calling identity with \
454                     `Binder.clearCallingIdentity()`, but the next statement is not a `try` \
455                     statement.
456 
457                     Use the following pattern for running operations with your own identity:
458 
459                     ```
460                     final long token = Binder.clearCallingIdentity();
461                     try {
462                         // Code using your own identity
463                     } finally {
464                         Binder.restoreCallingIdentity(token);
465                     }
466                     ```
467 
468                     Any calls/operations between `Binder.clearCallingIdentity()` and `try` \
469                     statement risk throwing an exception without doing a safe and unconditional \
470                     restore of the identity with `Binder.restoreCallingIdentity()` as an immediate \
471                     child of the `finally` block. If you do not follow the pattern, you may run \
472                     code with your identity that was originally intended to run with the calling \
473                     application's identity.
474                     """,
475                 category = Category.SECURITY,
476                 priority = 6,
477                 severity = Severity.WARNING,
478                 implementation = Implementation(
479                         CallingIdentityTokenDetector::class.java,
480                         Scope.JAVA_FILE_SCOPE
481                 )
482         )
483 
484         private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally(
485             variableName: String
486         ): String = "You cleared the calling identity and returned the result into " +
487                 "`$variableName`, but the next statement is not a `try`-`finally` statement. " +
488                 "Define a `try`-`finally` block after `$variableName` declaration to ensure a " +
489                 "safe restore of the calling identity by calling " +
490                 "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " +
491                 "of the `finally` block."
492 
493         /** Issue: Binder.restoreCallingIdentity() is not in finally block */
494         @JvmField
495         val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create(
496                 id = "RestoreIdentityCallNotInFinallyBlock",
497                 briefDescription = "Binder.restoreCallingIdentity() is not in finally block",
498                 explanation = """
499                     You are restoring the original calling identity with \
500                     `Binder.restoreCallingIdentity()`, but the call is not an immediate child of \
501                     the `finally` block of the `try` statement.
502 
503                     Use the following pattern for running operations with your own identity:
504 
505                     ```
506                     final long token = Binder.clearCallingIdentity();
507                     try {
508                         // Code using your own identity
509                     } finally {
510                         Binder.restoreCallingIdentity(token);
511                     }
512                     ```
513 
514                     If you do not surround the code using your identity with the `try` statement \
515                     and call `Binder.restoreCallingIdentity()` as an immediate child of the \
516                     `finally` block, you may run code with your identity that was originally \
517                     intended to run with the calling application's identity.
518                     """,
519                 category = Category.SECURITY,
520                 priority = 6,
521                 severity = Severity.WARNING,
522                 implementation = Implementation(
523                         CallingIdentityTokenDetector::class.java,
524                         Scope.JAVA_FILE_SCOPE
525                 )
526         )
527 
528         private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock(
529             variableName: String
530         ): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " +
531                 "the `finally` block of the try statement after `$variableName` declaration. " +
532                         "Surround the call with `finally` block and call it unconditionally."
533 
534         /** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */
535         @JvmField
536         val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create(
537                 id = "UseOfCallerAwareMethodsWithClearedIdentity",
538                 briefDescription = "Use of caller-aware methods after " +
539                         "Binder.clearCallingIdentity()",
540                 explanation = """
541                     You cleared the original calling identity with \
542                     `Binder.clearCallingIdentity()`, but used one of the methods below before \
543                     restoring the identity. These methods will use your own identity instead of \
544                     the caller's identity, so if this is expected replace them with methods that \
545                     explicitly query your own identity such as `Process.myUid()`, \
546                     `Process.myPid()` and `UserHandle.myUserId()`, otherwise move those methods \
547                     out of the `Binder.clearCallingIdentity()` / `Binder.restoreCallingIdentity()` \
548                     section.
549 
550                     ```
551                     Binder.getCallingPid()
552                     Binder.getCallingUid()
553                     Binder.getCallingUidOrThrow()
554                     Binder.getCallingUserHandle()
555                     UserHandle.getCallingAppId()
556                     UserHandle.getCallingUserId()
557                     ```
558                     """,
559                 category = Category.SECURITY,
560                 priority = 6,
561                 severity = Severity.WARNING,
562                 implementation = Implementation(
563                         CallingIdentityTokenDetector::class.java,
564                         Scope.JAVA_FILE_SCOPE
565                 )
566         )
567 
568         private fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
569             variableName: String,
570             methodName: String
571         ): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " +
572                 "and returned into `$variableName`, so `$methodName` will be using your own " +
573                 "identity instead of the caller's. Either explicitly query your own identity or " +
574                 "move it after restoring the identity with " +
575                 "`Binder.restoreCallingIdentity($variableName)`."
576     }
577 }
578