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