Update lint checks to their state on internal master
GlobalLintChecker was added as a prebuilt already on aosp, but we
could make it a source build if we had the source on aosp.
Bug: 264451752
Test: Presubmits
Change-Id: Iea5e5d2adef8c261490e14269cf6737c2a369e6d
diff --git a/tools/lint/framework/Android.bp b/tools/lint/framework/Android.bp
new file mode 100644
index 0000000..30a6daa
--- /dev/null
+++ b/tools/lint/framework/Android.bp
@@ -0,0 +1,64 @@
+// Copyright (C) 2022 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",
+ ],
+ static_libs: [
+ "AndroidCommonLint",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+ name: "AndroidFrameworkLintCheckerTest",
+ srcs: ["checks/src/test/java/**/*.kt"],
+ static_libs: [
+ "AndroidFrameworkLintChecker",
+ "junit",
+ "lint",
+ "lint_tests",
+ ],
+ test_options: {
+ unit_test: true,
+ tradefed_options: [
+ {
+ // lint bundles in some classes that were built with older versions
+ // of libraries, and no longer load. Since tradefed tries to load
+ // all classes in the jar to look for tests, it crashes loading them.
+ // Exclude these classes from tradefed's search.
+ name: "exclude-paths",
+ value: "org/apache",
+ },
+ {
+ name: "exclude-paths",
+ value: "META-INF",
+ },
+ ],
+ },
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
new file mode 100644
index 0000000..b4f6a1c
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.parcel.SaferParcelChecker
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class AndroidFrameworkIssueRegistry : IssueRegistry() {
+ override val issues = listOf(
+ CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+ CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+ CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
+ SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
+ PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+ PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+ PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
+ )
+
+ override val api: Int
+ get() = CURRENT_API
+
+ override val minApi: Int
+ get() = 8
+
+ override val vendor: Vendor = Vendor(
+ vendorName = "Android",
+ feedbackUrl = "http://b/issues/new?component=315013",
+ contact = "brufino@google.com"
+ )
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
new file mode 100644
index 0000000..0c375c3
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
@@ -0,0 +1,648 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.search.PsiSearchScopeUtil
+import com.intellij.psi.search.SearchScope
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.ULocalVariable
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.UTryExpression
+import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.getQualifiedParentOrThis
+import org.jetbrains.uast.getUCallExpression
+import org.jetbrains.uast.skipParenthesizedExprDown
+import org.jetbrains.uast.skipParenthesizedExprUp
+
+/**
+ * Lint Detector that finds issues with improper usages of the token returned by
+ * Binder.clearCallingIdentity()
+ */
+@Suppress("UnstableApiUsage")
+class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
+ /** Map of <Token variable name, Token object> */
+ private val tokensMap = mutableMapOf<String, Token>()
+
+ override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+ listOf(ULocalVariable::class.java, UCallExpression::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
+ * - Checks for clearCallingIdentity() not followed by try-finally issue
+ * - Stores token variable name, scope in the file, location and finally block in tokensMap
+ */
+ override fun visitLocalVariable(node: ULocalVariable) {
+ val initializer = node.uastInitializer?.skipParenthesizedExprDown()
+ val rhsExpression = initializer?.getUCallExpression() ?: 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
+ )
+ )
+ }
+ // If the next statement in the tree is not a try-finally statement, we need to report
+ // the "clearCallingIdentity() is not followed by try-finally" issue
+ val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression)
+ ?.finallyClause
+ if (finallyClause == null) {
+ context.report(
+ ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ location,
+ getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName)
+ )
+ }
+ tokensMap[variableName] = Token(
+ variableName,
+ node.sourcePsi?.getUseScope(),
+ location,
+ finallyClause
+ )
+ }
+
+ override fun visitCallExpression(node: UCallExpression) {
+ when {
+ isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> {
+ checkClearCallingIdentityCall(node)
+ }
+ isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> {
+ checkRestoreCallingIdentityCall(node)
+ }
+ isCallerAwareMethod(node) -> checkCallerAwareMethod(node)
+ }
+ }
+
+ private fun checkClearCallingIdentityCall(node: UCallExpression) {
+ var firstNonQualifiedParent = getFirstNonQualifiedParent(node)
+ // if the call expression is inside a ternary, and the ternary is assigned
+ // to a variable, then we are still technically assigning
+ // any result of clearCallingIdentity to a variable
+ if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) {
+ firstNonQualifiedParent = firstNonQualifiedParent.uastParent
+ }
+ if (firstNonQualifiedParent !is ULocalVariable) {
+ context.report(
+ ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+ context.getLocation(node),
+ getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+ node.getQualifiedParentOrThis().asRenderString()
+ )
+ )
+ }
+ }
+
+ private fun checkCallerAwareMethod(node: UCallExpression) {
+ val token = findFirstTokenInScope(node)
+ if (token != null) {
+ context.report(
+ ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ context.getLocation(node),
+ getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
+ token.variableName,
+ node.asRenderString()
+ )
+ )
+ }
+ }
+
+ /**
+ * - Checks for restoreCallingIdentity() not in the finally block issue
+ * - Removes token from tokensMap if token is within the scope of the method
+ */
+ private fun checkRestoreCallingIdentityCall(node: UCallExpression) {
+ val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return
+ val variableName = arg.identifier
+ val originalScope = tokensMap[variableName]?.scope ?: return
+ val psi = arg.sourcePsi ?: return
+ // Checks if Binder.restoreCallingIdentity(token) is called within the scope of the
+ // token declaration. If not within the scope, no action is needed because the token is
+ // irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
+ if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
+ // We do not report "restore identity call not in finally" issue when there is no
+ // finally block because that case is already handled by "clear identity call not
+ // followed by try-finally" issue
+ if (tokensMap[variableName]?.finallyBlock != null &&
+ getFirstNonQualifiedParent(node) !=
+ tokensMap[variableName]?.finallyBlock
+ ) {
+ context.report(
+ ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ context.getLocation(node),
+ getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName)
+ )
+ }
+ tokensMap.remove(variableName)
+ }
+
+ private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? {
+ // UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
+ // receiver.selector, so to get the call's immediate parent we need to get the topmost
+ // parent qualified reference expression and access its parent
+ return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent)
+ }
+
+ private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
+ callerAwareMethods.any { method -> isMethodCall(expression, method) }
+
+ private fun isMethodCall(
+ expression: UCallExpression,
+ method: Method
+ ): Boolean {
+ val psiMethod = expression.resolve() ?: return false
+ return psiMethod.getName() == method.methodName &&
+ context.evaluator.methodMatches(
+ psiMethod,
+ method.className,
+ /* allowInherit */ true,
+ *method.args
+ )
+ }
+
+ /**
+ * ULocalVariable in the file tree:
+ *
+ * UBlockExpression
+ * UDeclarationsExpression
+ * ULocalVariable
+ * ULocalVariable
+ * UTryStatement
+ * etc.
+ *
+ * To get the next statement of ULocalVariable:
+ * - If there exists a next sibling in UDeclarationsExpression, return the sibling
+ * - If there exists a next sibling of UDeclarationsExpression in UBlockExpression, return
+ * the sibling
+ * - Otherwise, return null
+ *
+ * Example 1 - the next sibling is in UDeclarationsExpression:
+ * Code:
+ * {
+ * int num1 = 0, num2 = methodThatThrowsException();
+ * }
+ * Returns: num2 = methodThatThrowsException()
+ *
+ * Example 2 - the next sibling is in UBlockExpression:
+ * Code:
+ * {
+ * int num1 = 0;
+ * methodThatThrowsException();
+ * }
+ * Returns: methodThatThrowsException()
+ *
+ * Example 3 - no next sibling;
+ * Code:
+ * {
+ * int num1 = 0;
+ * }
+ * Returns: null
+ */
+ private fun getNextStatementOfLocalVariable(node: ULocalVariable): UElement? {
+ val declarationsExpression = node.uastParent as? UDeclarationsExpression ?: return null
+ val declarations = declarationsExpression.declarations
+ val indexInDeclarations = declarations.indexOf(node)
+ if (indexInDeclarations != -1 && declarations.size > indexInDeclarations + 1) {
+ return declarations[indexInDeclarations + 1]
+ }
+ val enclosingBlock = node
+ .getParentOfType<UBlockExpression>(strict = true) ?: return null
+ val expressions = enclosingBlock.expressions
+ val indexInBlock = expressions.indexOf(declarationsExpression as UElement)
+ return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1)
+ }
+ }
+
+ 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<String>
+ ) {
+ 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,
+ val finallyBlock: UElement?
+ )
+
+ companion object {
+ const val CLASS_BINDER = "android.os.Binder"
+ const val CLASS_USER_HANDLE = "android.os.UserHandle"
+
+ private 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
+ )
+
+ /** 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
+ )
+ )
+
+ private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " +
+ "not been used to restore the calling identity. Introduce a `try`-`finally` " +
+ "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " +
+ "in `finally` 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
+ )
+ )
+
+ private 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
+ )
+ )
+
+ private 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)`."
+
+ private fun getIncidentMessageNestedClearIdentityCallsSecondary(
+ firstCallVariableName: String
+ ): String = "Location of the `$firstCallVariableName` declaration."
+
+ /** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */
+ @JvmField
+ val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create(
+ id = "ClearIdentityCallNotFollowedByTryFinally",
+ briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " +
+ "statement",
+ explanation = """
+ You cleared the original calling identity with \
+ `Binder.clearCallingIdentity()`, but the next statement is not a `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(token);
+ }
+ ```
+
+ Any calls/operations between `Binder.clearCallingIdentity()` and `try` \
+ statement risk throwing an exception without doing a safe and unconditional \
+ restore of the identity with `Binder.restoreCallingIdentity()` as an immediate \
+ child of the `finally` block. If you do not follow the pattern, 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
+ )
+ )
+
+ private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally(
+ variableName: String
+ ): String = "You cleared the calling identity and returned the result into " +
+ "`$variableName`, but the next statement is not a `try`-`finally` statement. " +
+ "Define a `try`-`finally` block after `$variableName` declaration to ensure a " +
+ "safe restore of the calling identity by calling " +
+ "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " +
+ "of the `finally` block."
+
+ /** 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 an immediate child of \
+ 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(token);
+ }
+ ```
+
+ If you do not surround the code using your identity with the `try` statement \
+ and call `Binder.restoreCallingIdentity()` as an immediate child of 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
+ )
+ )
+
+ private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock(
+ variableName: String
+ ): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " +
+ "the `finally` block of the try statement after `$variableName` declaration. " +
+ "Surround the call with `finally` block and call it unconditionally."
+
+ /** 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
+ )
+ )
+
+ private 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)`."
+
+ /** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */
+ @JvmField
+ val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create(
+ id = "ResultOfClearIdentityCallNotStoredInVariable",
+ briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " +
+ "variable",
+ explanation = """
+ You cleared the original calling identity with \
+ `Binder.clearCallingIdentity()`, but did not store the result of the method \
+ call in a variable. You need to store the result in a variable and restore it later.
+
+ 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(token);
+ }
+ ```
+ """,
+ category = Category.SECURITY,
+ priority = 6,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ CallingIdentityTokenDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+ methodName: String
+ ): String = "You cleared the original identity with `$methodName` but did not store the " +
+ "result in a variable. You need to store the result in a variable and restore it " +
+ "later."
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
new file mode 100644
index 0000000..fe567da
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+/**
+ * Lint Detector that finds issues with improper usages of the non-user getter methods of Settings
+ */
+@Suppress("UnstableApiUsage")
+class CallingSettingsNonUserGetterMethodsDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> = listOf(
+ "getString",
+ "getInt",
+ "getLong",
+ "getFloat"
+ )
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val evaluator = context.evaluator
+ if (evaluator.isMemberInClass(method, "android.provider.Settings.Secure") ||
+ evaluator.isMemberInClass(method, "android.provider.Settings.System")
+ ) {
+ val message = getIncidentMessageNonUserGetterMethods(getMethodSignature(method))
+ context.report(ISSUE_NON_USER_GETTER_CALLED, node, context.getNameLocation(node),
+ message)
+ }
+ }
+
+ private fun getMethodSignature(method: PsiMethod) =
+ method.containingClass
+ ?.qualifiedName
+ ?.let { "$it#${method.name}" }
+ ?: method.name
+
+ companion object {
+ @JvmField
+ val ISSUE_NON_USER_GETTER_CALLED: Issue = Issue.create(
+ id = "NonUserGetterCalled",
+ briefDescription = "Non-ForUser Getter Method called to Settings",
+ explanation = """
+ System process should not call the non-ForUser getter methods of \
+ `Settings.Secure` or `Settings.System`. For example, instead of \
+ `Settings.Secure.getInt()`, use `Settings.Secure.getIntForUser()` instead. \
+ This will make sure that the correct Settings value is retrieved.
+ """,
+ category = Category.CORRECTNESS,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ CallingSettingsNonUserGetterMethodsDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ fun getIncidentMessageNonUserGetterMethods(methodSignature: String) =
+ "`$methodSignature()` called from system process. " +
+ "Please call `${methodSignature}ForUser()` instead. "
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
new file mode 100644
index 0000000..48540b1d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.client.api.UastParser
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Detector
+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.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.interprocedural.CallGraph
+import com.android.tools.lint.detector.api.interprocedural.CallGraphResult
+import com.android.tools.lint.detector.api.interprocedural.searchForPaths
+import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.PsiMethod
+import java.util.LinkedList
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * A lint checker to detect potential package visibility issues for system's APIs. APIs working
+ * in the system_server and taking the package name as a parameter may have chance to reveal
+ * package existence status on the device, and break the
+ * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility">
+ * Package Visibility</a> that we introduced in Android 11.
+ * <p>
+ * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance
+ * to detect package existence state on the device from the result of the API, if there is no
+ * package visibility filtering rule or uid identify checks applying to the parameter of the
+ * package name.
+ */
+class PackageVisibilityDetector : Detector(), SourceCodeScanner {
+
+ // Enables call graph analysis
+ override fun isCallGraphRequired(): Boolean = true
+
+ override fun analyzeCallGraph(
+ context: Context,
+ callGraph: CallGraphResult
+ ) {
+ val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi)
+ val sinkMethodNodes = callGraph.callGraph.nodes.filter {
+ // TODO(b/228285232): Remove enforce permission sink methods
+ isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS)
+ }
+ val parser = context.client.getUastParser(context.project)
+ analyzeApisContainPackageNameParameters(
+ context, parser, systemServerApiNodes, sinkMethodNodes)
+ }
+
+ /**
+ * Looking for API contains package name parameters, report the lint issue if the API does not
+ * invoke any sink methods.
+ */
+ private fun analyzeApisContainPackageNameParameters(
+ context: Context,
+ parser: UastParser,
+ systemServerApiNodes: List<CallGraph.Node>,
+ sinkMethodNodes: List<CallGraph.Node>
+ ) {
+ for (apiNode in systemServerApiNodes) {
+ val apiMethod = apiNode.getUMethod() ?: continue
+ val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param ->
+ if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) {
+ index
+ } else {
+ null
+ }
+ }.takeIf(List<Int>::isNotEmpty) ?: continue
+
+ for (pkgNameParamIndex in pkgNameParamIndexes) {
+ // Trace the call path of the method's argument, pass the lint checks if a sink
+ // method is found
+ if (traceArgumentCallPath(
+ apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) {
+ continue
+ }
+ // Pass the check if one of the sink methods is invoked
+ if (hasValidPath(
+ searchForPaths(
+ sources = listOf(apiNode),
+ isSink = { it in sinkMethodNodes },
+ getNeighbors = { node -> node.edges.map { it.node!! } }
+ )
+ )
+ ) continue
+
+ // Report issue
+ val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement
+ val location = parser.createLocation(reportElement)
+ context.report(
+ ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+ location,
+ getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex)
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns {@code true} if the method associated with the given node is a system server's
+ * public API that extends from Stub class.
+ */
+ private fun isSystemServerApi(
+ node: CallGraph.Node
+ ): Boolean {
+ val method = node.getUMethod() ?: return false
+ if (!method.hasModifierProperty("public") ||
+ method.uastBody == null ||
+ method.containingClass is PsiAnonymousClass) {
+ return false
+ }
+ val className = method.containingClass?.qualifiedName ?: return false
+ if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+ return false
+ }
+ return (method.containingClass ?: return false).supers
+ .filter { it.name == CLASS_STUB }
+ .filter { it.qualifiedName !in BYPASS_STUBS }
+ .any { it.findMethodBySignature(method, /* checkBases */ true) != null }
+ }
+
+ /**
+ * Returns {@code true} if the list contains the node of the call graph.
+ */
+ private fun isNodeInList(
+ node: CallGraph.Node,
+ filters: List<Method>
+ ): Boolean {
+ val method = node.getUMethod() ?: return false
+ return Method(method) in filters
+ }
+
+ /**
+ * Trace the call paths of the argument of the method in the start entry. Return {@code true}
+ * if one of methods in the sink call list is invoked.
+ * Take an example of the call path:
+ * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess()
+ * It returns {@code true} if the filterAppAccess() is in the sink call list.
+ */
+ private fun traceArgumentCallPath(
+ apiNode: CallGraph.Node,
+ pkgNameParamIndex: Int,
+ sinkList: List<Method>
+ ): Boolean {
+ val startEntry = TraceEntry(apiNode, pkgNameParamIndex)
+ val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) }
+ val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) }
+ while (!traceQueue.isEmpty()) {
+ val entry = traceQueue.poll()
+ val entryNode = entry.node
+ val entryMethod = entryNode.getUMethod() ?: continue
+ val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name
+ for (outEdge in entryNode.edges) {
+ val outNode = outEdge.node ?: continue
+ val outMethod = outNode.getUMethod() ?: continue
+ val outArgumentIndex =
+ outEdge.call?.findArgumentIndex(
+ entryArgumentName, outMethod.uastParameters.size)
+ val sinkMethod = findInSinkList(outMethod, sinkList)
+ if (sinkMethod == null) {
+ if (outArgumentIndex == null) {
+ // Path is not relevant to the sink method and argument
+ continue
+ }
+ // Path is relevant to the argument, add a new trace entry if never visit before
+ val newEntry = TraceEntry(outNode, outArgumentIndex)
+ if (newEntry !in allVisits) {
+ traceQueue.add(newEntry)
+ allVisits.add(newEntry)
+ }
+ continue
+ }
+ if (sinkMethod.matchArgument && outArgumentIndex == null) {
+ // The sink call is required to match the argument, but not found
+ continue
+ }
+ if (sinkMethod.checkCaller &&
+ entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) {
+ // The sink call is in the scope of Binder.clearCallingIdentify
+ continue
+ }
+ // A sink method is matched
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Returns the UMethod associated with the given node of call graph.
+ */
+ private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod
+
+ /**
+ * Returns the system module name (e.g. com.android.server.pm) of the method of the
+ * call graph node.
+ */
+ private fun CallGraph.Node.getModuleName(): String? {
+ val method = getUMethod() ?: return null
+ val className = method.containingClass?.qualifiedName ?: return null
+ if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+ return null
+ }
+ val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length)
+ if (dotPos == -1) {
+ return SYSTEM_PACKAGE_PREFIX
+ }
+ return className.substring(0, dotPos)
+ }
+
+ /**
+ * Return {@code true} if the argument in the method's body is in-use.
+ */
+ private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean {
+ val method = getUMethod() ?: return false
+ val argumentName = method.uastParameters[argIndex].name
+ var foundArg = false
+ val methodVisitor = object : AbstractUastVisitor() {
+ override fun visitSimpleNameReferenceExpression(
+ node: USimpleNameReferenceExpression
+ ): Boolean {
+ if (node.identifier == argumentName) {
+ foundArg = true
+ }
+ return true
+ }
+ }
+ method.uastBody?.accept(methodVisitor)
+ return foundArg
+ }
+
+ /**
+ * Given an argument name, returns the index of argument in the call expression.
+ */
+ private fun UCallExpression.findArgumentIndex(
+ argumentName: String,
+ parameterSize: Int
+ ): Int? {
+ if (valueArgumentCount == 0 || parameterSize == 0) {
+ return null
+ }
+ var match = false
+ val argVisitor = object : AbstractUastVisitor() {
+ override fun visitSimpleNameReferenceExpression(
+ node: USimpleNameReferenceExpression
+ ): Boolean {
+ if (node.identifier == argumentName) {
+ match = true
+ }
+ return true
+ }
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ return true
+ }
+ }
+ valueArguments.take(parameterSize).forEachIndexed { index, argument ->
+ argument.accept(argVisitor)
+ if (match) {
+ return index
+ }
+ }
+ return null
+ }
+
+ /**
+ * Given a UMethod, returns a method from the sink method list.
+ */
+ private fun findInSinkList(
+ uMethod: UMethod,
+ sinkCallList: List<Method>
+ ): Method? {
+ return sinkCallList.find {
+ it == Method(uMethod) ||
+ it == Method(uMethod.containingClass?.qualifiedName ?: "", "*")
+ }
+ }
+
+ /**
+ * Returns {@code true} if the call expression is in the scope of the
+ * Binder.clearCallingIdentify.
+ */
+ private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean {
+ var isInScope = false
+ val methodVisitor = object : AbstractUastVisitor() {
+ private var clearCallingIdentity = 0
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ if (call == node && clearCallingIdentity != 0) {
+ isInScope = true
+ return true
+ }
+ val visitMethod = Method(node.resolve() ?: return false)
+ if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) {
+ clearCallingIdentity++
+ } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) {
+ clearCallingIdentity--
+ }
+ return false
+ }
+ }
+ accept(methodVisitor)
+ return isInScope
+ }
+
+ /**
+ * Checks the module name of the start node and the last node that invokes the sink method
+ * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same
+ * module name for both nodes.
+ */
+ private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean {
+ for (pathNodes in paths) {
+ if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) {
+ continue
+ }
+ val startModule = pathNodes[0].getModuleName() ?: continue
+ val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue
+ if (startModule == lastCallModule) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * A data class to represent the method.
+ */
+ private data class Method(
+ val clazz: String,
+ val name: String
+ ) {
+ // Used by traceArgumentCallPath to indicate that the method is required to match the
+ // argument name
+ var matchArgument = true
+
+ // Used by traceArgumentCallPath to indicate that the method is required to check whether
+ // the Binder.clearCallingIdentity is invoked.
+ var checkCaller = false
+
+ constructor(
+ clazz: String,
+ name: String,
+ matchArgument: Boolean = true,
+ checkCaller: Boolean = false
+ ) : this(clazz, name) {
+ this.matchArgument = matchArgument
+ this.checkCaller = checkCaller
+ }
+
+ constructor(
+ method: PsiMethod
+ ) : this(method.containingClass?.qualifiedName ?: "", method.name)
+
+ constructor(
+ method: com.google.android.lint.model.Method
+ ) : this(method.clazz, method.name)
+ }
+
+ /**
+ * A data class to represent the parameter of the method. The parameter name is converted to
+ * lower case letters for comparison.
+ */
+ private data class Parameter private constructor(
+ val typeName: String,
+ val parameterName: String
+ ) {
+ constructor(uParameter: UParameter) : this(
+ uParameter.type.canonicalText,
+ uParameter.name.lowercase()
+ )
+
+ companion object {
+ fun create(typeName: String, parameterName: String) =
+ Parameter(typeName, parameterName.lowercase())
+ }
+ }
+
+ /**
+ * A data class wraps a method node of the call graph and an index that indicates an
+ * argument of the method to record call trace information.
+ */
+ private data class TraceEntry(
+ val node: CallGraph.Node,
+ val argumentIndex: Int
+ )
+
+ companion object {
+ private const val SYSTEM_PACKAGE_PREFIX = "com.android.server."
+ // A valid call path list needs to contain a start node and a sink node
+ private const val VALID_CALL_PATH_NODES_SIZE = 2
+
+ private const val CLASS_STRING = "java.lang.String"
+ private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager"
+ private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager"
+ private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager"
+ private const val CLASS_BINDER = "android.os.Binder"
+ private const val CLASS_PACKAGE_MANAGER_INTERNAL =
+ "android.content.pm.PackageManagerInternal"
+
+ // Patterns of package name parameter
+ private val PACKAGE_NAME_PATTERNS = setOf(
+ Parameter.create(CLASS_STRING, "packageName"),
+ Parameter.create(CLASS_STRING, "callingPackage"),
+ Parameter.create(CLASS_STRING, "callingPackageName"),
+ Parameter.create(CLASS_STRING, "pkgName"),
+ Parameter.create(CLASS_STRING, "callingPkg"),
+ Parameter.create(CLASS_STRING, "pkg")
+ )
+
+ // Package manager APIs
+ private val PACKAGE_NAME_SINK_METHOD_LIST = listOf(
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false),
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"),
+ Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"),
+ Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true),
+ Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true),
+ Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false),
+ Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false)
+ )
+
+ // AppOps APIs which include uid and package visibility filters checks
+ private val APPOPS_METHODS = listOf(
+ Method(CLASS_APPOPS_MANAGER, "noteOp"),
+ Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "noteOperation"),
+ Method(CLASS_APPOPS_MANAGER, "noteProxyOp"),
+ Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "startOp"),
+ Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"),
+ Method(CLASS_APPOPS_MANAGER, "FinishOp"),
+ Method(CLASS_APPOPS_MANAGER, "finishProxyOp"),
+ Method(CLASS_APPOPS_MANAGER, "checkPackage")
+ )
+
+ // Enforce permission APIs
+ private val ENFORCE_PERMISSION_METHODS =
+ com.google.android.lint.ENFORCE_PERMISSION_METHODS
+ .map(PackageVisibilityDetector::Method)
+
+ private val BYPASS_STUBS = listOf(
+ "android.content.pm.IPackageDataObserver.Stub",
+ "android.content.pm.IPackageDeleteObserver.Stub",
+ "android.content.pm.IPackageDeleteObserver2.Stub",
+ "android.content.pm.IPackageInstallObserver2.Stub",
+ "com.android.internal.app.IAppOpsCallback.Stub",
+
+ // TODO(b/228285637): Do not bypass PackageManagerService API
+ "android.content.pm.IPackageManager.Stub",
+ "android.content.pm.IPackageManagerNative.Stub"
+ )
+
+ private val METHOD_CLEAR_CALLING_IDENTITY =
+ Method(CLASS_BINDER, "clearCallingIdentity")
+ private val METHOD_RESTORE_CALLING_IDENTITY =
+ Method(CLASS_BINDER, "restoreCallingIdentity")
+
+ private fun getMsgPackageNameNoPackageVisibilityFilters(
+ method: UMethod,
+ argumentIndex: Int
+ ): String = "Api: ${method.name} contains a package name parameter: " +
+ "${method.uastParameters[argumentIndex].name} does not apply " +
+ "package visibility filtering rules."
+
+ private val EXPLANATION = """
+ APIs working in the system_server and taking the package name as a parameter may have
+ chance to reveal package existence status on the device, and break the package
+ visibility that we introduced in Android 11.
+ (https://developer.android.com/about/versions/11/privacy/package-visibility)
+
+ Take an example of the API `boolean setFoo(String packageName)`, a malicious app may
+ have chance to get package existence state on the device from the result of the API,
+ if there is no package visibility filtering rule or uid identify checks applying to
+ the parameter of the package name.
+
+ To resolve it, you could apply package visibility filtering rules to the package name
+ via PackageManagerInternal.filterAppAccess API, before starting to use the package name.
+ If the parameter is a calling package name, use the PackageManager API such as
+ PackageManager.getPackagesForUid to verify the calling identify.
+ """
+
+ val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create(
+ id = "ApiMightLeakAppVisibility",
+ briefDescription = "Api takes package name parameter doesn't apply " +
+ "package visibility filters",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 1,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PackageVisibilityDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 0000000..e12ec3d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> =
+ listOf(UAnnotation::class.java, UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler =
+ PermissionMethodHandler(context)
+
+ private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ if (hasPermissionMethodAnnotation(node)) return
+ if (onlyCallsPermissionMethod(node)) {
+ val location = context.getLocation(node.javaPsi.modifierList)
+ val fix = fix()
+ .annotate(ANNOTATION_PERMISSION_METHOD)
+ .range(location)
+ .autoFix()
+ .build()
+
+ context.report(
+ ISSUE_CAN_BE_PERMISSION_METHOD,
+ location,
+ "Annotate method with @PermissionMethod",
+ fix
+ )
+ }
+ }
+
+ override fun visitAnnotation(node: UAnnotation) {
+ if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+ val method = node.getContainingUMethod() ?: return
+
+ if (!isPermissionMethodReturnType(method)) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` should return `void`, \
+ `boolean`, or `@PackageManager.PermissionResult int`."
+ """.trimIndent()
+ )
+ }
+
+ if (method.returnType == PsiType.INT &&
+ method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+ ) {
+ context.report(
+ ISSUE_PERMISSION_METHOD_USAGE,
+ context.getLocation(node),
+ """
+ Methods annotated with `@PermissionMethod` that return `int` should \
+ also be annotated with `@PackageManager.PermissionResult.`"
+ """.trimIndent()
+ )
+ }
+ }
+ }
+
+ companion object {
+
+ private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+ `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+ Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+ `void` and potentially throw `SecurityException`.
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+ id = "PermissionMethodUsage",
+ briefDescription = "@PermissionMethod used incorrectly",
+ explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = true
+ )
+
+ private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+ Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+ be annotated with @PermissionMethod. For example:
+ ```
+ void wrapperHelper() {
+ // Context.enforceCallingPermission is annotated with @PermissionMethod
+ context.enforceCallingPermission(SOME_PERMISSION)
+ }
+ ```
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+ id = "CanBePermissionMethod",
+ briefDescription = "Method can be annotated with @PermissionMethod",
+ explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ PermissionMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = false
+ )
+
+ private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+ listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+ /**
+ * Identifies methods that...
+ * DO call other methods annotated with @PermissionMethod
+ * DO NOT do anything else
+ */
+ private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+ val body = method.uastBody as? UBlockExpression ?: return false
+ if (body.expressions.isEmpty()) return false
+ for (expression in body.expressions) {
+ when (expression) {
+ is UQualifiedReferenceExpression -> {
+ if (!isPermissionMethodCall(expression.selector)) return false
+ }
+ is UReturnExpression -> {
+ if (!isPermissionMethodCall(expression.returnExpression)) return false
+ }
+ is UCallExpression -> {
+ if (!isPermissionMethodCall(expression)) return false
+ }
+ is UIfExpression -> {
+ if (expression.thenExpression !is UReturnExpression) return false
+ if (!isPermissionMethodCall(expression.condition)) return false
+ }
+ else -> return false
+ }
+ }
+ return true
+ }
+
+ private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+ return when (expression) {
+ is UQualifiedReferenceExpression ->
+ return isPermissionMethodCall(expression.selector)
+ is UCallExpression -> {
+ val calledMethod = expression.resolve()?.getUMethod() ?: return false
+ return hasPermissionMethodAnnotation(calledMethod)
+ }
+ else -> false
+ }
+ }
+
+ private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+ .any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) }
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
new file mode 100644
index 0000000..06c098d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiCallExpression
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiIntersectionType
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import com.intellij.psi.PsiWildcardType
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UVariable
+
+/**
+ * Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue
+ * with a fix that migrates towards the new safer API by appending an argument in the form of
+ * {@code com.package.ItemType.class} coming from the result of the overridden method.
+ */
+abstract class CallMigrator(
+ val method: Method,
+ private val rejects: Set<String> = emptySet(),
+) {
+ open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) {
+ val location = context.getLocation(call)
+ val itemType = filter(getBoundingClass(context, call, method))
+ val fix = (itemType as? PsiClassType)?.let { type ->
+ getParcelFix(location, this.method.name, getArgumentSuffix(type))
+ }
+ val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage"
+ context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix)
+ }
+
+ protected open fun getArgumentSuffix(type: PsiClassType) =
+ ", ${type.rawType().canonicalText}.class"
+
+ protected open fun getBoundingClass(
+ context: JavaContext,
+ call: UCallExpression,
+ method: PsiMethod,
+ ): PsiType? = null
+
+ protected fun getItemType(type: PsiType, container: String): PsiClassType? {
+ val supers = getParentTypes(type).mapNotNull { it as? PsiClassType }
+ val containerType = supers.firstOrNull { it.rawType().canonicalText == container }
+ ?: return null
+ val itemType = containerType.parameters.getOrNull(0) ?: return null
+ // TODO: Expand to other types, see PsiTypeVisitor
+ return when (itemType) {
+ is PsiClassType -> itemType
+ is PsiWildcardType -> itemType.bound as PsiClassType
+ else -> null
+ }
+ }
+
+ /**
+ * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}.
+ *
+ * This could be an assignment, an argument passed to a method call, to a constructor call, a
+ * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned.
+ */
+ protected fun getReceivingType(expression: UElement): PsiType? {
+ val parent = expression.uastParent
+ var type = when (parent) {
+ is UCallExpression -> {
+ val i = parent.valueArguments.indexOf(expression)
+ val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null
+ val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor
+ val method = psiCall.resolveMethod()!!
+ method.getSignature(typeSubstitutor).parameterTypes[i]
+ }
+ is UVariable -> parent.type
+ is UExpression -> parent.getExpressionType()
+ else -> null
+ }
+ if (type == null && expression is UExpression) {
+ type = expression.getExpressionType()
+ }
+ return type
+ }
+
+ protected fun filter(type: PsiType?): PsiType? {
+ // It's important that PsiIntersectionType case is above the one that check the type in
+ // rejects, because for intersect types, the canonicalText is one of the terms.
+ if (type is PsiIntersectionType) {
+ return type.conjuncts.mapNotNull(this::filter).firstOrNull()
+ }
+ if (type == null || type.canonicalText in rejects) {
+ return null
+ }
+ if (type is PsiClassType && type.resolve() is PsiTypeParameter) {
+ return null
+ }
+ return type
+ }
+
+ private fun getParentTypes(type: PsiType): Set<PsiType> =
+ type.superTypes.flatMap(::getParentTypes).toSet() + type
+
+ protected fun getParcelFix(location: Location, method: String, arguments: String) =
+ LintFix
+ .create()
+ .name("Migrate to safer Parcel.$method() API")
+ .replace()
+ .range(location)
+ .pattern("$method\\s*\\(((?:.|\\n)*)\\)")
+ .with("\\k<1>$arguments")
+ .autoFix()
+ .build()
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the {@code argument}-th argument.
+ */
+class ContainerArgumentMigrator(
+ method: Method,
+ private val argument: Int,
+ private val container: String,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null
+ return getItemType(firstParamType, container)!!
+ }
+
+ /**
+ * We need to insert a casting construct in the class parameter. For example:
+ * (Class<Foo<Bar>>) (Class<?>) Foo.class.
+ * This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and
+ * class type is Class<Foo?).
+ */
+ override fun getArgumentSuffix(type: PsiClassType): String {
+ if (type.parameters.isNotEmpty()) {
+ val rawType = type.rawType()
+ return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class"
+ }
+ return super.getArgumentSuffix(type)
+ }
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the return type of the method.
+ */
+class ContainerReturnMigrator(
+ method: Method,
+ private val container: String,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val type = getReceivingType(call.uastParent!!) ?: return null
+ return getItemType(type, container)
+ }
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected type for the method result.
+ */
+class ReturnMigrator(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ return getReceivingType(call.uastParent!!)
+ }
+}
+
+/**
+ * This class appends the class loader and the class object by deriving the type from the method
+ * result.
+ */
+class ReturnMigratorWithClassLoader(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ return getReceivingType(call.uastParent!!)
+ }
+
+ override fun getArgumentSuffix(type: PsiClassType): String =
+ "${type.rawType().canonicalText}.class.getClassLoader(), " +
+ "${type.rawType().canonicalText}.class"
+
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected array type
+ * for the method result.
+ */
+class ArrayReturnMigrator(
+ method: Method,
+ rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+ override fun getBoundingClass(
+ context: JavaContext, call: UCallExpression, method: PsiMethod
+ ): PsiType? {
+ val type = getReceivingType(call.uastParent!!)
+ return (type as? PsiArrayType)?.componentType
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
new file mode 100644
index 0000000..0826e8e
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+data class Method(
+ val params: List<String>,
+ val clazz: String,
+ val name: String,
+ val parameters: List<String>
+) {
+ constructor(
+ clazz: String,
+ name: String,
+ parameters: List<String>
+ ) : this(
+ listOf(), clazz, name, parameters
+ )
+
+ val signature: String
+ get() {
+ val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} "
+ return "$prefix$clazz.$name(${parameters.joinToString()})"
+ }
+
+ val className: String by lazy {
+ clazz.split(".").last()
+ }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
new file mode 100644
index 0000000..f928263
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.*
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiSubstitutor
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import org.jetbrains.uast.UCallExpression
+import java.util.*
+
+@Suppress("UnstableApiUsage")
+class SaferParcelChecker : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> =
+ MIGRATORS
+ .map(CallMigrator::method)
+ .map(Method::name)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!isAtLeastT(context)) return
+ val signature = getSignature(method)
+ val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return
+ migrator.report(context, node, method)
+ }
+
+ private fun getSignature(method: PsiMethod): String {
+ val name = UastLintUtils.getQualifiedName(method)
+ val signature = method.getSignature(PsiSubstitutor.EMPTY)
+ val parameters =
+ signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText)
+ val types = signature.typeParameters.map(PsiTypeParameter::getName)
+ val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " "
+ return "$prefix$name($parameters)"
+ }
+
+ private fun isAtLeastT(context: Context): Boolean {
+ val project = if (context.isGlobalAnalysis()) context.mainProject else context.project
+ return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create(
+ id = "UnsafeParcelApi",
+ briefDescription = "Use of unsafe deserialization API",
+ explanation = """
+ You are using a deprecated deserialization API that doesn't accept the expected class as\
+ a parameter. This means that unexpected classes could be instantiated and\
+ unexpected code executed.
+
+ Please migrate to the safer alternative that takes an extra Class<T> parameter.
+ """,
+ category = Category.SECURITY,
+ priority = 8,
+ severity = Severity.WARNING,
+
+ implementation = Implementation(
+ SaferParcelChecker::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ // Parcel
+ private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf())
+ private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader"))
+ private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader"))
+
+ // Bundle
+ private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String"))
+ private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String"))
+
+ // Intent
+ private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String"))
+ private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String"))
+
+ // TODO: Write migrators for methods below
+ private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader"))
+
+ private val MIGRATORS = listOf(
+ ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")),
+ ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"),
+ ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"),
+ ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"),
+ ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"),
+ ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE),
+ ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")),
+ ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+
+ ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")),
+ ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")),
+ ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")),
+
+ ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")),
+ ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")),
+ ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")),
+ ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")),
+ )
+ }
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
new file mode 100644
index 0000000..d90f3e3
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
@@ -0,0 +1,867 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+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<Issue> = listOf(
+ CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+ CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+ CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+ CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+ CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ /** No issue scenario */
+
+ fun testDoesNotDetectIssuesInCorrectScenario() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ 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);
+ }
+ final long token3 = clearCallingIdentity();
+ try {
+ } finally {
+ restoreCallingIdentity(token3);
+ }
+ final Long token4 = true ? Binder.clearCallingIdentity() : null;
+ try {
+ } finally {
+ if (token4 != null) {
+ restoreCallingIdentity(token4);
+ }
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ /** Unused token issue tests */
+
+ fun testDetectsUnusedTokens() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ private void testMethodImported() {
+ final long token1 = Binder.clearCallingIdentity();
+ try {
+ } finally {
+ }
+ }
+ private void testMethodFullClass() {
+ final long token2 = android.os.Binder.clearCallingIdentity();
+ try {
+ } finally {
+ }
+ }
+ private void testMethodChildOfBinder() {
+ final long token3 = clearCallingIdentity();
+ try {
+ } finally {
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \
+ restore the calling identity. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token1) in finally or \
+ remove token1. [UnusedTokenOfOriginalCallingIdentity]
+ final long token1 = Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:11: Warning: token2 has not been used to \
+ restore the calling identity. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token2) in finally or \
+ remove token2. [UnusedTokenOfOriginalCallingIdentity]
+ final long token2 = android.os.Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:17: Warning: token3 has not been used to \
+ restore the calling identity. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token3) in finally or \
+ remove token3. [UnusedTokenOfOriginalCallingIdentity]
+ final long token3 = clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDetectsUnusedTokensInScopes() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 {
+ private void testMethodTokenFromClearIdentity() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ } finally {
+ }
+ }
+ private void testMethodTokenNotFromClearIdentity() {
+ 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. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token) in finally 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 testMethodTokenFromClearIdentity() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ private void testMethodTokenNotFromClearIdentity() {
+ 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. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token) in finally 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. Introduce a try-finally after the \
+ declaration and call Binder.restoreCallingIdentity(token) in finally 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 extends Binder {
+ 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);
+ }
+ long token3 = clearCallingIdentity();
+ try {
+ } finally {
+ restoreCallingIdentity(token3);
+ }
+ }
+ }
+ """
+ ).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();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:15: Warning: token3 is a non-final token from \
+ Binder.clearCallingIdentity(). Add final keyword to token3. \
+ [NonFinalTokenOfOriginalCallingIdentity]
+ long token3 = clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ /** Nested clearCallingIdentity() calls issue tests */
+
+ fun testDetectsNestedClearCallingIdentityCalls() {
+ // Pattern: clear - clear - clear - restore - restore - restore
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ private void testMethod() {
+ final long token1 = Binder.clearCallingIdentity();
+ try {
+ final long token2 = android.os.Binder.clearCallingIdentity();
+ try {
+ final long token3 = clearCallingIdentity();
+ try {
+ } finally {
+ restoreCallingIdentity(token3);
+ }
+ } finally {
+ android.os.Binder.restoreCallingIdentity(token2);
+ }
+ } finally {
+ 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.
+ src/test/pkg/TestClass1.java:9: 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 = clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:5: Location of the token1 declaration.
+ 0 errors, 2 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ /** clearCallingIdentity() not followed by try-finally issue tests */
+
+ fun testDetectsClearIdentityCallNotFollowedByTryFinally() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder{
+ private void testMethodNoTry() {
+ final long token = Binder.clearCallingIdentity();
+ Binder.restoreCallingIdentity(token);
+ }
+ private void testMethodSomethingBetweenClearAndTry() {
+ final long token = Binder.clearCallingIdentity();
+ int pid = 0;
+ try {
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ private void testMethodLocalVariableBetweenClearAndTry() {
+ final long token = clearCallingIdentity(), num = 0;
+ try {
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+ private void testMethodTryCatch() {
+ final long token = android.os.Binder.clearCallingIdentity();
+ try {
+ } catch (Exception e) {
+ }
+ Binder.restoreCallingIdentity(token);
+ }
+ private void testMethodTryCatchInScopes() {
+ final long token = android.os.Binder.clearCallingIdentity();
+ {
+ try {
+ } catch (Exception e) {
+ }
+ }
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Warning: You cleared the calling identity \
+ and returned the result into token, but the next statement is not a \
+ try-finally statement. Define a try-finally block after token declaration \
+ to ensure a safe restore of the calling identity by calling \
+ Binder.restoreCallingIdentity(token) and making it an immediate child of \
+ the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+ final long token = Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:9: Warning: You cleared the calling identity \
+ and returned the result into token, but the next statement is not a \
+ try-finally statement. Define a try-finally block after token declaration \
+ to ensure a safe restore of the calling identity by calling \
+ Binder.restoreCallingIdentity(token) and making it an immediate child of \
+ the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+ final long token = Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:17: Warning: You cleared the calling identity \
+ and returned the result into token, but the next statement is not a \
+ try-finally statement. Define a try-finally block after token declaration \
+ to ensure a safe restore of the calling identity by calling \
+ Binder.restoreCallingIdentity(token) and making it an immediate child of \
+ the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+ final long token = clearCallingIdentity(), num = 0;
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:24: Warning: You cleared the calling identity \
+ and returned the result into token, but the next statement is not a \
+ try-finally statement. Define a try-finally block after token declaration \
+ to ensure a safe restore of the calling identity by calling \
+ Binder.restoreCallingIdentity(token) and making it an immediate child of \
+ the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+ final long token = android.os.Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:31: Warning: You cleared the calling identity \
+ and returned the result into token, but the next statement is not a \
+ try-finally statement. Define a try-finally block after token declaration \
+ to ensure a safe restore of the calling identity by calling \
+ Binder.restoreCallingIdentity(token) and making it an immediate child of \
+ the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+ final long token = android.os.Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 5 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 extends Binder {
+ private void testMethodImported() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ } catch (Exception e) {
+ } finally {
+ }
+ Binder.restoreCallingIdentity(token);
+ }
+ private void testMethodFullClass() {
+ final long token = android.os.Binder.clearCallingIdentity();
+ try {
+ } finally {
+ }
+ android.os.Binder.restoreCallingIdentity(token);
+ }
+ private void testMethodRestoreInCatch() {
+ final long token = clearCallingIdentity();
+ try {
+ } catch (Exception e) {
+ restoreCallingIdentity(token);
+ } finally {
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:10: Warning: \
+ Binder.restoreCallingIdentity(token) is not an immediate child of the \
+ finally block of the try statement after token declaration. Surround the c\
+ all with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ Binder.restoreCallingIdentity(token);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:17: Warning: \
+ Binder.restoreCallingIdentity(token) is not an immediate child of the \
+ finally block of the try statement after token declaration. Surround the c\
+ all with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ android.os.Binder.restoreCallingIdentity(token);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:23: Warning: \
+ Binder.restoreCallingIdentity(token) is not an immediate child of the \
+ finally block of the try statement after token declaration. Surround the c\
+ all with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ restoreCallingIdentity(token);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ private void testMethodOutsideFinally() {
+ final long token1 = Binder.clearCallingIdentity();
+ try {
+ } catch (Exception e) {
+ } finally {
+ }
+ {
+ Binder.restoreCallingIdentity(token1);
+ }
+ final long token2 = android.os.Binder.clearCallingIdentity();
+ try {
+ } finally {
+ }
+ {
+ {
+ {
+ android.os.Binder.restoreCallingIdentity(token2);
+ }
+ }
+ }
+ }
+ private void testMethodInsideFinallyInScopes() {
+ final long token1 = Binder.clearCallingIdentity();
+ try {
+ } finally {
+ {
+ {
+ Binder.restoreCallingIdentity(token1);
+ }
+ }
+ }
+ final long token2 = clearCallingIdentity();
+ try {
+ } finally {
+ if (true) restoreCallingIdentity(token2);
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:11: Warning: \
+ Binder.restoreCallingIdentity(token1) is not an immediate child of the \
+ finally block of the try statement after token1 declaration. Surround the \
+ call with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ Binder.restoreCallingIdentity(token1);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:20: Warning: \
+ Binder.restoreCallingIdentity(token2) is not an immediate child of the \
+ finally block of the try statement after token2 declaration. Surround the \
+ call with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ android.os.Binder.restoreCallingIdentity(token2);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:31: Warning: \
+ Binder.restoreCallingIdentity(token1) is not an immediate child of the \
+ finally block of the try statement after token1 declaration. Surround the \
+ call with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ Binder.restoreCallingIdentity(token1);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:38: Warning: \
+ Binder.restoreCallingIdentity(token2) is not an immediate child of the \
+ finally block of the try statement after token2 declaration. Surround the \
+ call with finally block and call it unconditionally. \
+ [RestoreIdentityCallNotInFinallyBlock]
+ if (true) restoreCallingIdentity(token2);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 4 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ /** 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();
+ try {
+ int pid1 = Binder.getCallingPid();
+ int pid2 = android.os.Binder.getCallingPid();
+ int uid1 = Binder.getCallingUid();
+ int uid2 = android.os.Binder.getCallingUid();
+ 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:8: Warning: You cleared the original identity \
+ with Binder.clearCallingIdentity() and returned into token, so \
+ 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:9: Warning: You cleared the original identity \
+ with Binder.clearCallingIdentity() and returned into token, so \
+ 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:10: Warning: You cleared the original \
+ identity with Binder.clearCallingIdentity() and returned into token, so \
+ 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:11: Warning: You cleared the original \
+ identity with Binder.clearCallingIdentity() and returned into token, so \
+ 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 \
+ 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 \
+ 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 \
+ 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 \
+ 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 \
+ 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 \
+ 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 \
+ 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 \
+ 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()
+ )
+ }
+
+ /** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */
+
+ fun testDetectsResultOfClearIdentityCallNotStoredInVariable() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Binder;
+ public class TestClass1 extends Binder {
+ private void testMethod() {
+ Binder.clearCallingIdentity();
+ android.os.Binder.clearCallingIdentity();
+ clearCallingIdentity();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \
+ with Binder.clearCallingIdentity() but did not store the result in a \
+ variable. You need to store the result in a variable and restore it later. \
+ [ResultOfClearIdentityCallNotStoredInVariable]
+ Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \
+ with android.os.Binder.clearCallingIdentity() but did not store the result \
+ in a variable. You need to store the result in a variable and restore it \
+ later. [ResultOfClearIdentityCallNotStoredInVariable]
+ android.os.Binder.clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \
+ with clearCallingIdentity() but did not store the result in a variable. \
+ You need to store the result in a variable and restore it later. \
+ [ResultOfClearIdentityCallNotStoredInVariable]
+ clearCallingIdentity();
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 3 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", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
new file mode 100644
index 0000000..e72f384
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class CallingSettingsNonUserGetterMethodsIssueDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = CallingSettingsNonUserGetterMethodsDetector()
+
+ override fun getIssues(): List<Issue> = listOf(
+ CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testDoesNotDetectIssues() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.provider.Settings.Secure;
+ public class TestClass1 {
+ private void testMethod(Context context) {
+ final int value = Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.KEY1, 0, 0);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDetectsNonUserGetterCalledFromSecure() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.provider.Settings.Secure;
+ public class TestClass1 {
+ private void testMethod(Context context) {
+ final int value = Secure.getInt(context.getContentResolver(),
+ Settings.Secure.KEY1);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Error: \
+ android.provider.Settings.Secure#getInt() called from system process. \
+ Please call android.provider.Settings.Secure#getIntForUser() instead. \
+ [NonUserGetterCalled]
+ final int value = Secure.getInt(context.getContentResolver(),
+ ~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+ fun testDetectsNonUserGetterCalledFromSystem() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.provider.Settings.System;
+ public class TestClass1 {
+ private void testMethod(Context context) {
+ final float value = System.getFloat(context.getContentResolver(),
+ Settings.System.KEY1);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Error: \
+ android.provider.Settings.System#getFloat() called from system process. \
+ Please call android.provider.Settings.System#getFloatForUser() instead. \
+ [NonUserGetterCalled]
+ final float value = System.getFloat(context.getContentResolver(),
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDetectsNonUserGetterCalledFromSettings() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.provider.Settings;
+ public class TestClass1 {
+ private void testMethod(Context context) {
+ float value = Settings.System.getFloat(context.getContentResolver(),
+ Settings.System.KEY1);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:5: Error: \
+ android.provider.Settings.System#getFloat() called from system process. \
+ Please call android.provider.Settings.System#getFloatForUser() instead. \
+ [NonUserGetterCalled]
+ float value = Settings.System.getFloat(context.getContentResolver(),
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDetectsNonUserGettersCalledFromSystemAndSecure() {
+ lint().files(
+ java(
+ """
+ package test.pkg;
+ import android.provider.Settings.Secure;
+ import android.provider.Settings.System;
+ public class TestClass1 {
+ private void testMethod(Context context) {
+ final long value1 = Secure.getLong(context.getContentResolver(),
+ Settings.Secure.KEY1, 0);
+ final String value2 = System.getString(context.getContentResolver(),
+ Settings.System.KEY2);
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass1.java:6: Error: \
+ android.provider.Settings.Secure#getLong() called from system process. \
+ Please call android.provider.Settings.Secure#getLongForUser() instead. \
+ [NonUserGetterCalled]
+ final long value1 = Secure.getLong(context.getContentResolver(),
+ ~~~~~~~
+ src/test/pkg/TestClass1.java:8: Error: \
+ android.provider.Settings.System#getString() called from system process. \
+ Please call android.provider.Settings.System#getStringForUser() instead. \
+ [NonUserGetterCalled]
+ final String value2 = System.getString(context.getContentResolver(),
+ ~~~~~~~~~
+ 2 errors, 0 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ private val SettingsStub: TestFile = java(
+ """
+ package android.provider;
+ public class Settings {
+ public class Secure {
+ float getFloat(ContentResolver cr, String key) {
+ return 0.0f;
+ }
+ long getLong(ContentResolver cr, String key) {
+ return 0l;
+ }
+ int getInt(ContentResolver cr, String key) {
+ return 0;
+ }
+ }
+ public class System {
+ float getFloat(ContentResolver cr, String key) {
+ return 0.0f;
+ }
+ long getLong(ContentResolver cr, String key) {
+ return 0l;
+ }
+ String getString(ContentResolver cr, String key) {
+ return null;
+ }
+ }
+ }
+ """
+ ).indented()
+
+ private val stubs = arrayOf(SettingsStub)
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
new file mode 100644
index 0000000..a70644a
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class PackageVisibilityDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = PackageVisibilityDetector()
+
+ override fun getIssues(): MutableList<Issue> = mutableListOf(
+ PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ @Override
+ public boolean hasPackage(String packageName) {
+ return packageName != null;
+ }
+ }
+ """).indented(), *stubs
+ ).run().expect(
+ """
+ src/com/android/server/lint/test/TestClass.java:6: Warning: \
+ Api: hasPackage contains a package name parameter: packageName does not apply \
+ package visibility filtering rules. \
+ [ApiMightLeakAppVisibility]
+ public boolean hasPackage(String packageName) {
+ ~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesAppOps() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.app.AppOpsManager;
+ import android.os.Binder;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private AppOpsManager mAppOpsManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ checkPackage(packageName);
+ return packageName != null;
+ }
+
+ private void checkPackage(String packageName) {
+ mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesEnforcePermission() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.Context;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private Context mContext;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ enforcePermission();
+ return packageName != null;
+ }
+
+ private void enforcePermission() {
+ mContext.checkCallingPermission(
+ android.Manifest.permission.ACCESS_INPUT_FLINGER);
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDoesNotDetectIssuesApiInvokesPackageManager() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.pm.PackageInfo;
+ import android.content.pm.PackageManager;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ private PackageManager mPackageManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ return getPackageInfo(packageName) != null;
+ }
+
+ private PackageInfo getPackageInfo(String packageName) {
+ try {
+ return mPackageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() {
+ lint().files(java(
+ """
+ package com.android.server.lint.test;
+ import android.content.pm.PackageInfo;
+ import android.content.pm.PackageManager;
+ import android.internal.test.IFoo;import android.os.Binder;
+
+ public class TestClass extends IFoo.Stub {
+ private PackageManager mPackageManager;
+
+ @Override
+ public boolean hasPackage(String packageName) {
+ return getPackageInfo(packageName) != null;
+ }
+
+ private PackageInfo getPackageInfo(String packageName) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ try {
+ return mPackageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ } finally{
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+ """).indented(), *stubs
+ ).run().expect(
+ """
+ src/com/android/server/lint/test/TestClass.java:10: Warning: \
+ Api: hasPackage contains a package name parameter: packageName does not apply \
+ package visibility filtering rules. \
+ [ApiMightLeakAppVisibility]
+ public boolean hasPackage(String packageName) {
+ ~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() {
+ lint().files(java(
+ """
+ package com.test.not.system.prefix;
+ import android.internal.test.IFoo;
+
+ public class TestClass extends IFoo.Stub {
+ @Override
+ public boolean hasPackage(String packageName) {
+ return packageName != null;
+ }
+ }
+ """
+ ).indented(), *stubs).run().expectClean()
+ }
+
+ private val contextStub: TestFile = java(
+ """
+ package android.content;
+
+ public abstract class Context {
+ public abstract int checkCallingPermission(String permission);
+ }
+ """
+ ).indented()
+
+ private val appOpsManagerStub: TestFile = java(
+ """
+ package android.app;
+
+ public class AppOpsManager {
+ public void checkPackage(int uid, String packageName) {
+ }
+ }
+ """
+ ).indented()
+
+ private val packageManagerStub: TestFile = java(
+ """
+ package android.content.pm;
+ import android.content.pm.PackageInfo;
+
+ public abstract class PackageManager {
+ public static class NameNotFoundException extends AndroidException {
+ }
+
+ public abstract PackageInfo getPackageInfo(String packageName, int flags)
+ throws NameNotFoundException;
+ }
+ """
+ ).indented()
+
+ private val packageInfoStub: TestFile = java(
+ """
+ package android.content.pm;
+ public class PackageInfo {}
+ """
+ ).indented()
+
+ private val binderStub: TestFile = java(
+ """
+ package android.os;
+
+ public class Binder {
+ public static final native long clearCallingIdentity();
+ public static final native void restoreCallingIdentity(long token);
+ public static final native int getCallingUid();
+ }
+ """
+ ).indented()
+
+ private val interfaceIFooStub: TestFile = java(
+ """
+ package android.internal.test;
+ import android.os.Binder;
+
+ public interface IFoo {
+ boolean hasPackage(String packageName);
+ public abstract static class Stub extends Binder implements IFoo {
+ }
+ }
+ """
+ ).indented()
+
+ private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub,
+ packageInfoStub, binderStub, interfaceIFooStub)
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
new file mode 100644
index 0000000..e686695
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
@@ -0,0 +1,823 @@
+/*
+ * Copyright (C) 2022 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.google.android.lint.parcel
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class SaferParcelCheckerTest : LintDetectorTest() {
+ override fun getDetector(): Detector = SaferParcelChecker()
+
+ override fun getIssues(): List<Issue> = listOf(
+ SaferParcelChecker.ISSUE_UNSAFE_API_USAGE
+ )
+
+ override fun lint(): TestLintTask =
+ super.lint()
+ .allowMissingSdk(true)
+ // We don't do partial analysis in the platform
+ .skipTestModes(TestMode.PARTIAL)
+
+ /** Parcel Tests */
+
+ fun testParcelDetectUnsafeReadSerializable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+ import java.io.Serializable;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Serializable ans = p.readSerializable();
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .expectIdenticalTestModeOutput(false)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \
+ API usage [UnsafeParcelApi]
+ Serializable ans = p.readSerializable();
+ ~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadSerializable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+ import java.io.Serializable;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ String ans = p.readSerializable(null, String.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ ArrayList ans = p.readArrayList(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \
+ usage [UnsafeParcelApi]
+ ArrayList ans = p.readArrayList(null);
+ ~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ ArrayList<Intent> ans = p.readArrayList(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ p.readList(list, null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \
+ [UnsafeParcelApi]
+ p.readList(list, null);
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testDParceloesNotDetectSafeReadList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ p.readList(list, null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent ans = p.readParcelable(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \
+ usage [UnsafeParcelApi]
+ Intent ans = p.readParcelable(null);
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent ans = p.readParcelable(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelableList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ List<Intent> ans = p.readParcelableList(list, null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \
+ API usage [UnsafeParcelApi]
+ List<Intent> ans = p.readParcelableList(list, null);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelableList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import java.util.List;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ List<Intent> list = new ArrayList<Intent>();
+ List<Intent> ans =
+ p.readParcelableList(list, null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadSparseArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import android.util.SparseArray;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ SparseArray<Intent> ans = p.readSparseArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\
+ usage [UnsafeParcelApi]
+ SparseArray<Intent> ans = p.readSparseArray(null);
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadSparseArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+ import android.util.SparseArray;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ SparseArray<Intent> ans =
+ p.readSparseArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadSArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\
+ usage [UnsafeParcelApi]
+ Intent[] ans = p.readArray(null);
+ ~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testParcelDetectUnsafeReadParcelableSArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readParcelableArray(null);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\
+ usage [UnsafeParcelApi]
+ Intent[] ans = p.readParcelableArray(null);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testParcelDoesNotDetectSafeReadParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Parcel;
+
+ public class TestClass {
+ private TestClass(Parcel p) {
+ Intent[] ans = p.readParcelableArray(null, Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ /** Bundle Tests */
+
+ fun testBundleDetectUnsafeGetParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent ans = b.getParcelable("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi]
+ Intent ans = b.getParcelable("key");
+ ~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelable() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent ans = b.getParcelable("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetParcelableArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ ArrayList<Intent> ans = b.getParcelableArrayList("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi]
+ ArrayList<Intent> ans = b.getParcelableArrayList("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelableArrayList() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent[] ans = b.getParcelableArray("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi]
+ Intent[] ans = b.getParcelableArray("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ Intent[] ans = b.getParcelableArray("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ fun testBundleDetectUnsafeGetSparseParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi]
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testBundleDoesNotDetectSafeGetSparseParcelableArray() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+ import android.os.Bundle;
+
+ public class TestClass {
+ private TestClass(Bundle b) {
+ SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+ /** Intent Tests */
+
+ fun testIntentDetectUnsafeGetParcelableExtra() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+
+ public class TestClass {
+ private TestClass(Intent i) {
+ Intent ans = i.getParcelableExtra("name");
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi]
+ Intent ans = i.getParcelableExtra("name");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """.addLineContinuation()
+ )
+ }
+
+ fun testIntentDoesNotDetectSafeGetParcelableExtra() {
+ lint()
+ .files(
+ java(
+ """
+ package test.pkg;
+ import android.content.Intent;
+
+ public class TestClass {
+ private TestClass(Intent i) {
+ Intent ans = i.getParcelableExtra("name", Intent.class);
+ }
+ }
+ """
+ ).indented(),
+ *includes
+ )
+ .run()
+ .expect("No warnings.")
+ }
+
+
+ /** Stubs for classes used for testing */
+
+
+ private val includes =
+ arrayOf(
+ manifest().minSdk("33"),
+ java(
+ """
+ package android.os;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.HashMap;
+
+ public final class Parcel {
+ // Deprecated
+ public Object[] readArray(ClassLoader loader) { return null; }
+ public ArrayList readArrayList(ClassLoader loader) { return null; }
+ public HashMap readHashMap(ClassLoader loader) { return null; }
+ public void readList(List outVal, ClassLoader loader) {}
+ public void readMap(Map outVal, ClassLoader loader) {}
+ public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; }
+ public Parcelable[] readParcelableArray(ClassLoader loader) { return null; }
+ public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; }
+ public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; }
+ public Serializable readSerializable() { return null; }
+ public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; }
+
+ // Replacements
+ public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; }
+ public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; }
+ public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {}
+ public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {}
+ public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; }
+ public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; }
+ public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; }
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.os;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.HashMap;
+
+ public final class Bundle {
+ // Deprecated
+ public <T extends Parcelable> T getParcelable(String key) { return null; }
+ public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; }
+ public Parcelable[] getParcelableArray(String key) { return null; }
+ public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; }
+
+ // Replacements
+ public <T> T getParcelable(String key, Class<T> clazz) { return null; }
+ public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; }
+ public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; }
+ public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; }
+
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.os;
+ public interface Parcelable {}
+ """
+ ).indented(),
+ java(
+ """
+ package android.content;
+ public class Intent implements Parcelable, Cloneable {
+ // Deprecated
+ public <T extends Parcelable> T getParcelableExtra(String name) { return null; }
+
+ // Replacements
+ public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; }
+
+ }
+ """
+ ).indented(),
+ java(
+ """
+ package android.util;
+ public class SparseArray<E> implements Cloneable {}
+ """
+ ).indented(),
+ )
+
+ // Substitutes "backslash + new line" with an empty string to imitate line continuation
+ private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}