diff --git a/services/Android.bp b/services/Android.bp index cc0fd98c70604..a2d6beb41d293 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -21,6 +21,9 @@ java_defaults { // "-XepPatchLocation:/tmp/refaster/", ], }, + lint: { + extra_check_modules: ["AndroidFrameworkLintChecker"], + }, } filegroup { diff --git a/tools/lint/Android.bp b/tools/lint/Android.bp new file mode 100644 index 0000000000000..dcbc32b4e2888 --- /dev/null +++ b/tools/lint/Android.bp @@ -0,0 +1,46 @@ +// Copyright (C) 2021 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "AndroidFrameworkLintChecker", + srcs: ["checks/src/main/java/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], +} + +// TODO: (b/162368644) Implement these (working in gradle) Kotlin Tests to run on Soong +//java_test_host { +// name: "AndroidFrameworkLintCheckerTest", +// srcs: [ +// "checks/src/test/java/**/*.kt", +// "checks/src/main/java/**/*.kt", +// ], +// plugins: ["auto_service_plugin"], +// static_libs: [ +// "auto_service_annotations", +// "lint_api", +// ], +//} diff --git a/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenDetector.kt new file mode 100644 index 0000000000000..4e308347a144c --- /dev/null +++ b/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenDetector.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2021 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.lint + +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_NESTED_CLEAR_IDENTITY_CALLS +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_NON_FINAL_TOKEN +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_UNUSED_TOKEN +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNestedClearIdentityCallsPrimary +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNestedClearIdentityCallsSecondary +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNonFinalToken +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageRestoreIdentityCallNotInFinallyBlock +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageUnusedToken +import com.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Context +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import com.intellij.psi.search.PsiSearchScopeUtil +import com.intellij.psi.search.SearchScope +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULocalVariable +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.USimpleNameReferenceExpression +import org.jetbrains.uast.UTryExpression +import org.jetbrains.uast.getParentOfType +import org.jetbrains.uast.isUastChildOf + +/** + * Lint Detector that finds issues with improper usages of the token returned by + * Binder.clearCallingIdentity() + */ +@Suppress("UnstableApiUsage") +class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { + private companion object { + const val CLASS_BINDER = "android.os.Binder" + const val CLASS_USER_HANDLE = "android.os.UserHandle" + + @JvmField + val callerAwareMethods = listOf( + Method.BINDER_GET_CALLING_PID, + Method.BINDER_GET_CALLING_UID, + Method.BINDER_GET_CALLING_UID_OR_THROW, + Method.BINDER_GET_CALLING_USER_HANDLE, + Method.USER_HANDLE_GET_CALLING_APP_ID, + Method.USER_HANDLE_GET_CALLING_USER_ID + ) + } + + /** Map of */ + private val tokensMap = mutableMapOf() + + override fun getApplicableUastTypes(): List> = + listOf(ULocalVariable::class.java, UQualifiedReferenceExpression::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = + TokenUastHandler(context) + + /** File analysis starts with a clear map */ + override fun beforeCheckFile(context: Context) { + tokensMap.clear() + } + + /** + * - If tokensMap has tokens after checking the file -> reports all locations as unused token + * issue incidents + * - File analysis ends with a clear map + */ + override fun afterCheckFile(context: Context) { + for (token in tokensMap.values) { + context.report( + ISSUE_UNUSED_TOKEN, + token.location, + getIncidentMessageUnusedToken(token.variableName) + ) + } + tokensMap.clear() + } + + /** UAST handler that analyses elements and reports incidents */ + private inner class TokenUastHandler(val context: JavaContext) : UElementHandler() { + /** + * For every variable initialization with Binder.clearCallingIdentity(): + * - Checks for non-final token issue + * - Checks for unused token issue within different scopes + * - Checks for nested calls of clearCallingIdentity() issue + * - Stores token variable name, scope in the file and its location in tokensMap + */ + override fun visitLocalVariable(node: ULocalVariable) { + val rhsExpression = node.uastInitializer as? UQualifiedReferenceExpression ?: return + if (!isMethodCall(rhsExpression, Method.BINDER_CLEAR_CALLING_IDENTITY)) return + val location = context.getLocation(node as UElement) + val variableName = node.getName() + if (!node.isFinal) { + context.report( + ISSUE_NON_FINAL_TOKEN, + location, + getIncidentMessageNonFinalToken(variableName) + ) + } + // If there exists an unused variable with the same name in the map, we can imply that + // we left the scope of the previous declaration, so we need to report the unused token + val oldToken = tokensMap[variableName] + if (oldToken != null) { + context.report( + ISSUE_UNUSED_TOKEN, + oldToken.location, + getIncidentMessageUnusedToken(oldToken.variableName) + ) + } + // If there exists a token in the same scope as the current new token, it means that + // clearCallingIdentity() has been called at least twice without immediate restoration + // of identity, so we need to report the nested call of clearCallingIdentity() + val firstCallToken = findFirstTokenInScope(node) + if (firstCallToken != null) { + context.report( + ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + createNestedLocation(firstCallToken, location), + getIncidentMessageNestedClearIdentityCallsPrimary( + firstCallToken.variableName, + variableName + ) + ) + } + tokensMap[variableName] = Token(variableName, node.sourcePsi?.getUseScope(), location) + } + + /** + * For every class.method(): + * - Checks use of caller-aware methods issue + * For every call of Binder.restoreCallingIdentity(token): + * - Checks for restoreCallingIdentity() not in the finally block issue + * - Removes token from tokensMap if token is within the scope of the method + */ + override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) { + val token = findFirstTokenInScope(node) + if (isCallerAwareMethod(node) && token != null) { + context.report( + ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + context.getLocation(node), + getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( + token.variableName, + node.asRenderString() + ) + ) + return + } + if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return + val selector = node.selector as UCallExpression + val arg = selector.valueArguments[0] as? USimpleNameReferenceExpression ?: return + val variableName = arg.identifier + if (!isInFinallyBlock(node)) { + context.report( + ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + context.getLocation(node), + getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName) + ) + } + val originalScope = tokensMap[variableName]?.scope ?: return + val psi = arg.sourcePsi ?: return + if (PsiSearchScopeUtil.isInScope(originalScope, psi)) { + tokensMap.remove(variableName) + } + } + + private fun isCallerAwareMethod(expression: UQualifiedReferenceExpression): Boolean = + callerAwareMethods.any { method -> isMethodCall(expression, method) } + + private fun isMethodCall( + expression: UQualifiedReferenceExpression, + method: Method + ): Boolean { + val psiMethod = expression.resolve() as? PsiMethod ?: return false + return psiMethod.getName() == method.methodName && + context.evaluator.methodMatches( + psiMethod, + method.className, + /* allowInherit */ true, + *method.args + ) + } + } + + private fun isInFinallyBlock(expression: UExpression): Boolean { + val tryExpression = expression.getParentOfType(strict = true) + ?: return false + return expression.isUastChildOf(tryExpression.finallyClause) + } + + private fun findFirstTokenInScope(node: UElement): Token? { + val psi = node.sourcePsi ?: return null + for (token in tokensMap.values) { + if (token.scope != null && PsiSearchScopeUtil.isInScope(token.scope, psi)) { + return token + } + } + return null + } + + /** + * Creates a new instance of the primary location with the secondary location + * + * Here, secondary location is the helper location that shows where the issue originated + * + * The detector reports locations as objects, so when we add a secondary location to a location + * that has multiple issues, the secondary location gets displayed every time a location is + * referenced. + * + * Example: + * 1: final long token1 = Binder.clearCallingIdentity(); + * 2: long token2 = Binder.clearCallingIdentity(); + * 3: Binder.restoreCallingIdentity(token1); + * 4: Binder.restoreCallingIdentity(token2); + * + * Explanation: + * token2 has 2 issues: NonFinal and NestedCalls + * + * Lint report without cloning Lint report with cloning + * line 2: [NonFinalIssue] line 2: [NonFinalIssue] + * line 1: [NestedCallsIssue] + * line 2: [NestedCallsIssue] line 2: [NestedCallsIssue] + * line 1: [NestedCallsIssue] line 1: [NestedCallsIssue] + */ + private fun createNestedLocation( + firstCallToken: Token, + secondCallTokenLocation: Location + ): Location { + return cloneLocation(secondCallTokenLocation) + .withSecondary( + cloneLocation(firstCallToken.location), + getIncidentMessageNestedClearIdentityCallsSecondary( + firstCallToken.variableName + ) + ) + } + + private fun cloneLocation(location: Location): Location { + // smart cast of location.start to 'Position' is impossible, because 'location.start' is a + // public API property declared in different module + val locationStart = location.start + return if (locationStart == null) { + Location.create(location.file) + } else { + Location.create(location.file, locationStart, location.end) + } + } + + private enum class Method( + val className: String, + val methodName: String, + val args: Array + ) { + BINDER_CLEAR_CALLING_IDENTITY(CLASS_BINDER, "clearCallingIdentity", emptyArray()), + BINDER_RESTORE_CALLING_IDENTITY(CLASS_BINDER, "restoreCallingIdentity", arrayOf("long")), + BINDER_GET_CALLING_PID(CLASS_BINDER, "getCallingPid", emptyArray()), + BINDER_GET_CALLING_UID(CLASS_BINDER, "getCallingUid", emptyArray()), + BINDER_GET_CALLING_UID_OR_THROW(CLASS_BINDER, "getCallingUidOrThrow", emptyArray()), + BINDER_GET_CALLING_USER_HANDLE(CLASS_BINDER, "getCallingUserHandle", emptyArray()), + USER_HANDLE_GET_CALLING_APP_ID(CLASS_USER_HANDLE, "getCallingAppId", emptyArray()), + USER_HANDLE_GET_CALLING_USER_ID(CLASS_USER_HANDLE, "getCallingUserId", emptyArray()) + } + + private data class Token( + val variableName: String, + val scope: SearchScope?, + val location: Location + ) +} diff --git a/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenIssueRegistry.kt b/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenIssueRegistry.kt new file mode 100644 index 0000000000000..a0e9249f2db84 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/android/lint/CallingIdentityTokenIssueRegistry.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 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.lint + +import com.android.tools.lint.client.api.IssueRegistry +// TODO: uncomment when lint API in Soong becomes 30.0+ +// import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class CallingIdentityTokenIssueRegistry : IssueRegistry() { + override val issues = listOf( + ISSUE_UNUSED_TOKEN, + ISSUE_NON_FINAL_TOKEN, + ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY + ) + + override val api: Int + get() = CURRENT_API + + override val minApi: Int + get() = 8 + +// TODO: uncomment when lint API in Soong becomes 30.0+ +// override val vendor: Vendor = Vendor( +// vendorName = "Android Open Source Project", +// feedbackUrl = "http://b/issues/new?component=315013", +// contact = "brufino@google.com" +// ) + + companion object { + /** Issue: unused token from Binder.clearCallingIdentity() */ + @JvmField + val ISSUE_UNUSED_TOKEN: Issue = Issue.create( + id = "UnusedTokenOfOriginalCallingIdentity", + briefDescription = "Unused token of Binder.clearCallingIdentity()", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()`, but have not used the returned token to \ + restore the identity. + + Call `Binder.restoreCallingIdentity(token)` in the `finally` block, at the end \ + of the method or when you need to restore the identity. + + `token` is the result of `Binder.clearCallingIdentity()` + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has not been " + + "used to restore the calling identity. Call " + + "`Binder.restoreCallingIdentity($variableName)` or remove `$variableName`." + + /** Issue: non-final token from Binder.clearCallingIdentity() */ + @JvmField + val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create( + id = "NonFinalTokenOfOriginalCallingIdentity", + briefDescription = "Non-final token of Binder.clearCallingIdentity()", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()`, but have not made the returned token `final`. + + The token should be `final` in order to prevent it from being overwritten, \ + which can cause problems when restoring the identity with \ + `Binder.restoreCallingIdentity(token)`. + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is a " + + "non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " + + "`$variableName`." + + /** Issue: nested calls of Binder.clearCallingIdentity() */ + @JvmField + val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create( + id = "NestedClearCallingIdentityCalls", + briefDescription = "Nested calls of Binder.clearCallingIdentity()", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()` twice without restoring identity with the \ + result of the first call. + + Make sure to restore the identity after each clear identity call. + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun getIncidentMessageNestedClearIdentityCallsPrimary( + firstCallVariableName: String, + secondCallVariableName: String + ): String = "The calling identity has already been cleared and returned into " + + "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " + + "restoring the calling identity with " + + "`Binder.restoreCallingIdentity($firstCallVariableName)`." + + fun getIncidentMessageNestedClearIdentityCallsSecondary( + firstCallVariableName: String + ): String = "Location of the `$firstCallVariableName` declaration." + + /** Issue: Binder.restoreCallingIdentity() is not in finally block */ + @JvmField + val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create( + id = "RestoreIdentityCallNotInFinallyBlock", + briefDescription = "Binder.restoreCallingIdentity() is not in finally block", + explanation = """ + You are restoring the original calling identity with \ + `Binder.restoreCallingIdentity()`, but the call is not in the `finally` block \ + of the `try` statement. + + Use the following pattern for running operations with your own identity: + + ``` + final long token = Binder.clearCallingIdentity(); + try { + // Code using your own identity + } finally { + Binder.restoreCallingIdentity(); + } + ``` + + If you do not surround the code using your identity with the `try` statement \ + and call `Binder.restoreCallingIdentity()` in the `finally` block, you may run \ + code with your identity that was originally intended to run with the calling \ + application's identity. + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName: String): String = + "`Binder.restoreCallingIdentity($variableName)` is not in the `finally` block. " + + "Surround the call with `finally` block." + + /** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */ + @JvmField + val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create( + id = "UseOfCallerAwareMethodsWithClearedIdentity", + briefDescription = "Use of caller-aware methods after " + + "Binder.clearCallingIdentity()", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()`, but used one of the methods below before \ + restoring the identity. These methods will use your own identity instead of \ + the caller's identity, so if this is expected replace them with methods that \ + explicitly query your own identity such as `Process.myUid()`, \ + `Process.myPid()` and `UserHandle.myUserId()`, otherwise move those methods \ + out of the `Binder.clearCallingIdentity()` / `Binder.restoreCallingIdentity()` \ + section. + + ``` + Binder.getCallingPid() + Binder.getCallingUid() + Binder.getCallingUidOrThrow() + Binder.getCallingUserHandle() + UserHandle.getCallingAppId() + UserHandle.getCallingUserId() + ``` + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( + variableName: String, + methodName: String + ): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " + + "and returned into `$variableName`, so `$methodName` will be using your own " + + "identity instead of the caller's. Either explicitly query your own identity or " + + "move it after restoring the identity with " + + "`Binder.restoreCallingIdentity($variableName)`." + } +} diff --git a/tools/lint/checks/src/test/java/com/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/checks/src/test/java/com/android/lint/CallingIdentityTokenDetectorTest.kt new file mode 100644 index 0000000000000..8a816097c91f8 --- /dev/null +++ b/tools/lint/checks/src/test/java/com/android/lint/CallingIdentityTokenDetectorTest.kt @@ -0,0 +1,747 @@ +/* + * Copyright (C) 2021 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.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class CallingIdentityTokenDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = CallingIdentityTokenDetector() + + override fun getIssues(): List = listOf( + CallingIdentityTokenIssueRegistry.ISSUE_UNUSED_TOKEN, + CallingIdentityTokenIssueRegistry.ISSUE_NON_FINAL_TOKEN, + CallingIdentityTokenIssueRegistry.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + CallingIdentityTokenIssueRegistry.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + CallingIdentityTokenIssueRegistry + .ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY + ) + + /** No issue scenario */ + + fun testDoesNotDetectIssuesInCorrectScenario() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + final long token1 = Binder.clearCallingIdentity(); + try { + } finally { + Binder.restoreCallingIdentity(token1); + } + final long token2 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + android.os.Binder.restoreCallingIdentity(token2); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /** Unused token issue tests */ + + fun testDetectsUnusedTokens() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token1 = Binder.clearCallingIdentity(); + } + private void testMethod2() { + final long token2 = android.os.Binder.clearCallingIdentity(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \ + restore the calling identity. Call Binder.restoreCallingIdentity(token1) \ + or remove token1. [UnusedTokenOfOriginalCallingIdentity] + final long token1 = Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:8: Warning: token2 has not been used to \ + restore the calling identity. Call Binder.restoreCallingIdentity(token2) \ + or remove token2. [UnusedTokenOfOriginalCallingIdentity] + final long token2 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + fun testDetectsUnusedTokensInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token = Binder.clearCallingIdentity(); + } + private void testMethod2() { + long token = 0; + try { + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ + restore the calling identity. Call Binder.restoreCallingIdentity(token) \ + or remove token. [UnusedTokenOfOriginalCallingIdentity] + final long token = Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectUsedTokensInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token = Binder.clearCallingIdentity(); + try { + } finally { + Binder.restoreCallingIdentity(token); + } + } + private void testMethod2() { + long token = 0; + try { + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDetectsUnusedTokensWithSimilarNamesInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token = Binder.clearCallingIdentity(); + try { + } finally { + } + } + private void testMethod2() { + final long token = Binder.clearCallingIdentity(); + try { + } finally { + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ + restore the calling identity. Call Binder.restoreCallingIdentity(token) \ + or remove token. [UnusedTokenOfOriginalCallingIdentity] + final long token = Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:11: Warning: token has not been used to \ + restore the calling identity. Call Binder.restoreCallingIdentity(token) \ + or remove token. [UnusedTokenOfOriginalCallingIdentity] + final long token = Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + /** Non-final token issue tests */ + + fun testDetectsNonFinalTokens() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + long token1 = Binder.clearCallingIdentity(); + try { + } finally { + Binder.restoreCallingIdentity(token1); + } + long token2 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + android.os.Binder.restoreCallingIdentity(token2); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: token1 is a non-final token from \ + Binder.clearCallingIdentity(). Add final keyword to token1. \ + [NonFinalTokenOfOriginalCallingIdentity] + long token1 = Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:10: Warning: token2 is a non-final token from \ + Binder.clearCallingIdentity(). Add final keyword to token2. \ + [NonFinalTokenOfOriginalCallingIdentity] + long token2 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + /** Nested clearCallingIdentity() calls issue tests */ + + fun testDetectsNestedClearCallingIdentityCallsPattern1() { + // Pattern: clear - clear - restore - clear - restore - restore + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + final long token1 = Binder.clearCallingIdentity(); + final long token2 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + android.os.Binder.restoreCallingIdentity(token1); + } + final long token3 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + android.os.Binder.restoreCallingIdentity(token2); + } + try { + } finally { + android.os.Binder.restoreCallingIdentity(token3); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:6: Warning: The calling identity has already \ + been cleared and returned into token1. Move token2 declaration after \ + restoring the calling identity with Binder.restoreCallingIdentity(token1). \ + [NestedClearCallingIdentityCalls] + final long token2 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:5: Location of the token1 declaration. + src/test/pkg/TestClass1.java:11: Warning: The calling identity has already \ + been cleared and returned into token2. Move token3 declaration after \ + restoring the calling identity with Binder.restoreCallingIdentity(token2). \ + [NestedClearCallingIdentityCalls] + final long token3 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:6: Location of the token2 declaration. + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + fun testDetectsNestedClearCallingIdentityCallsPattern2() { + // Pattern: clear - clear - clear - restore - restore - restore + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + final long token1 = Binder.clearCallingIdentity(); + final long token2 = android.os.Binder.clearCallingIdentity(); + final long token3 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + Binder.restoreCallingIdentity(token1); + } + try { + } finally { + android.os.Binder.restoreCallingIdentity(token2); + } + try { + } finally { + android.os.Binder.restoreCallingIdentity(token3); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:6: Warning: The calling identity has already \ + been cleared and returned into token1. Move token2 declaration after \ + restoring the calling identity with Binder.restoreCallingIdentity(token1). \ + [NestedClearCallingIdentityCalls] + final long token2 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:5: Location of the token1 declaration. + src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \ + been cleared and returned into token1. Move token3 declaration after \ + restoring the calling identity with Binder.restoreCallingIdentity(token1). \ + [NestedClearCallingIdentityCalls] + final long token3 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:5: Location of the token1 declaration. + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + fun testDetectsNestedClearCallingIdentityCallsInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + final long token1 = Binder.clearCallingIdentity(); + try { + final long token2 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + Binder.restoreCallingIdentity(token2); + } + } finally { + android.os.Binder.restoreCallingIdentity(token1); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \ + been cleared and returned into token1. Move token2 declaration after \ + restoring the calling identity with Binder.restoreCallingIdentity(token1). \ + [NestedClearCallingIdentityCalls] + final long token2 = android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:5: Location of the token1 declaration. + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + /** restoreCallingIdentity() call not in finally block issue tests */ + + fun testDetectsRestoreCallingIdentityCallNotInFinally() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token = Binder.clearCallingIdentity(); + try { + } catch (Exception e) { + } + Binder.restoreCallingIdentity(token); + } + private void testMethod2() { + final long token = android.os.Binder.clearCallingIdentity(); + try { + } finally { + } + android.os.Binder.restoreCallingIdentity(token); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:9: Warning: \ + Binder.restoreCallingIdentity(token) is not in the finally block. Surround \ + the call with finally block. [RestoreIdentityCallNotInFinallyBlock] + Binder.restoreCallingIdentity(token); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:16: Warning: \ + Binder.restoreCallingIdentity(token) is not in the finally block. Surround \ + the call with finally block. [RestoreIdentityCallNotInFinallyBlock] + android.os.Binder.restoreCallingIdentity(token); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod1() { + final long token = Binder.clearCallingIdentity(); + try { + } catch (Exception e) { + } + { + Binder.restoreCallingIdentity(token); + } + } + private void testMethod2() { + final long token = android.os.Binder.clearCallingIdentity(); + try { + } finally { + } + { + { + { + android.os.Binder.restoreCallingIdentity(token); + } + } + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:10: Warning: \ + Binder.restoreCallingIdentity(token) is not in the finally block. Surround \ + the call with finally block. [RestoreIdentityCallNotInFinallyBlock] + Binder.restoreCallingIdentity(token); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:21: Warning: \ + Binder.restoreCallingIdentity(token) is not in the finally block. Surround \ + the call with finally block. [RestoreIdentityCallNotInFinallyBlock] + android.os.Binder.restoreCallingIdentity(token); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 2 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectRestoreCallingIdentityCallInFinallyInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 { + private void testMethod() { + final long token1 = Binder.clearCallingIdentity(); + try { + } finally { + { + { + Binder.restoreCallingIdentity(token1); + } + } + } + final long token2 = android.os.Binder.clearCallingIdentity(); + try { + } finally { + { + { + android.os.Binder.restoreCallingIdentity(token2); + } + } + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /** Use of caller-aware methods after clearCallingIdentity() issue tests */ + + fun testDetectsUseOfCallerAwareMethodsWithClearedIdentityIssuesInScopes() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + import android.os.UserHandle; + public class TestClass1 { + private void testMethod() { + final long token = Binder.clearCallingIdentity(); + int pid1 = Binder.getCallingPid(); + int pid2 = android.os.Binder.getCallingPid(); + int uid1 = Binder.getCallingUid(); + int uid2 = android.os.Binder.getCallingUid(); + try { + int uid3 = Binder.getCallingUidOrThrow(); + int uid4 = android.os.Binder.getCallingUidOrThrow(); + UserHandle uh1 = Binder.getCallingUserHandle(); + UserHandle uh2 = android.os.Binder.getCallingUserHandle(); + { + int appId1 = UserHandle.getCallingAppId(); + int appId2 = android.os.UserHandle.getCallingAppId(); + int userId1 = UserHandle.getCallingUserId(); + int userId2 = android.os.UserHandle.getCallingUserId(); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \ + with Binder.clearCallingIdentity() and returned into token, so \ + Binder.getCallingPid() will be using your own identity instead of the \ + caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int pid1 = Binder.getCallingPid(); + ~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:8: Warning: You cleared the original identity \ + with Binder.clearCallingIdentity() and returned into token, so \ + android.os.Binder.getCallingPid() will be using your own identity instead \ + of the caller's. Either explicitly query your own identity or move it \ + after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int pid2 = android.os.Binder.getCallingPid(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:9: Warning: You cleared the original identity \ + with Binder.clearCallingIdentity() and returned into token, so \ + Binder.getCallingUid() will be using your own identity instead of the \ + caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int uid1 = Binder.getCallingUid(); + ~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:10: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + android.os.Binder.getCallingUid() will be using your own identity instead \ + of the caller's. Either explicitly query your own identity or move it \ + after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int uid2 = android.os.Binder.getCallingUid(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:12: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + Binder.getCallingUidOrThrow() will be using your own identity instead of \ + the caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int uid3 = Binder.getCallingUidOrThrow(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:13: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + android.os.Binder.getCallingUidOrThrow() will be using your own identity \ + instead of the caller's. Either explicitly query your own identity or move \ + it after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int uid4 = android.os.Binder.getCallingUidOrThrow(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:14: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + Binder.getCallingUserHandle() will be using your own identity instead of \ + the caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + UserHandle uh1 = Binder.getCallingUserHandle(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:15: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + android.os.Binder.getCallingUserHandle() will be using your own identity \ + instead of the caller's. Either explicitly query your own identity or move \ + it after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + UserHandle uh2 = android.os.Binder.getCallingUserHandle(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:17: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + UserHandle.getCallingAppId() will be using your own identity instead of \ + the caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int appId1 = UserHandle.getCallingAppId(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:18: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + android.os.UserHandle.getCallingAppId() will be using your own identity \ + instead of the caller's. Either explicitly query your own identity or move \ + it after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int appId2 = android.os.UserHandle.getCallingAppId(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:19: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + UserHandle.getCallingUserId() will be using your own identity instead of \ + the caller's. Either explicitly query your own identity or move it after \ + restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int userId1 = UserHandle.getCallingUserId(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:20: Warning: You cleared the original \ + identity with Binder.clearCallingIdentity() and returned into token, so \ + android.os.UserHandle.getCallingUserId() will be using your own identity \ + instead of the caller's. Either explicitly query your own identity or move \ + it after restoring the identity with Binder.restoreCallingIdentity(token). \ + [UseOfCallerAwareMethodsWithClearedIdentity] + int userId2 = android.os.UserHandle.getCallingUserId(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 12 warnings + """.addLineContinuation() + ) + } + + /** Stubs for classes used for testing */ + + private val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static final native long clearCallingIdentity() { + return 0; + } + public static final native void restoreCallingIdentity(long token) { + } + public static final native int getCallingPid() { + return 0; + } + public static final native int getCallingUid() { + return 0; + } + public static final int getCallingUidOrThrow() { + return 0; + } + public static final @NonNull UserHandle getCallingUserHandle() { + return UserHandle.of(UserHandle.getUserId(getCallingUid())); + } + } + """ + ).indented() + + private val userHandleStub: TestFile = java( + """ + package android.os; + import android.annotation.AppIdInt; + import android.annotation.UserIdInt; + public class UserHandle { + public static @AppIdInt int getCallingAppId() { + return getAppId(Binder.getCallingUid()); + } + public static @UserIdInt int getCallingUserId() { + return getUserId(Binder.getCallingUid()); + } + public static @UserIdInt int getUserId(int uid) { + return 0; + } + public static @AppIdInt int getAppId(int uid) { + return 0; + } + public static UserHandle of(@UserIdInt int userId) { + return new UserHandle(); + } + } + """ + ).indented() + + private val userIdIntStub: TestFile = java( + """ + package android.annotation; + public @interface UserIdInt { + } + """ + ).indented() + + private val appIdIntStub: TestFile = java( + """ + package android.annotation; + public @interface AppIdInt { + } + """ + ).indented() + + private val stubs = arrayOf(binderStub, userHandleStub, userIdIntStub, appIdIntStub) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +}