From afc3a227828fe73ceedaa352105e3fde9f43b899 Mon Sep 17 00:00:00 2001 From: mattgilbride Date: Tue, 22 Nov 2022 13:43:11 +0000 Subject: Create AndroidGlobalLintChecker This change creates a separate lint jar "AndroidGlobalLintChecker". This set of custom checks that is intended to be global. These checks should run across the entire tree, not just the framework. - separate lint checking code broadly into "common", "framework", and "global" - publish AndroidGlobalLintChecker into the android distribution for consumption by prebuilts/cmdline-tools Bug: 236558918 Test: AndroidGlobalLintCheckerTest Change-Id: Ib1606a7bc8adfab2f13974e7366288a4e44bbfa2 --- tools/lint/Android.bp | 59 -- .../android/lint/AndroidFrameworkIssueRegistry.kt | 62 -- .../android/lint/CallingIdentityTokenDetector.kt | 648 -------------- .../CallingSettingsNonUserGetterMethodsDetector.kt | 83 -- .../main/java/com/google/android/lint/Constants.kt | 40 - .../android/lint/PackageVisibilityDetector.kt | 515 ------------ .../android/lint/PermissionMethodDetector.kt | 197 ----- .../android/lint/RegisterReceiverFlagDetector.kt | 927 --------------------- .../lint/aidl/AidlImplementationDetector.kt | 52 -- .../java/com/google/android/lint/aidl/Constants.kt | 73 -- .../android/lint/aidl/EnforcePermissionDetector.kt | 255 ------ .../android/lint/aidl/EnforcePermissionFix.kt | 137 --- .../lint/aidl/EnforcePermissionHelperDetector.kt | 130 --- .../android/lint/aidl/EnforcePermissionUtils.kt | 67 -- .../SimpleManualPermissionEnforcementDetector.kt | 150 ---- .../java/com/google/android/lint/model/Method.kt | 26 - .../google/android/lint/parcel/CallMigrators.kt | 229 ----- .../java/com/google/android/lint/parcel/Method.kt | 42 - .../android/lint/parcel/SaferParcelChecker.kt | 126 --- .../lint/CallingIdentityTokenDetectorTest.kt | 867 ------------------- ...ettingsNonUserGetterMethodsIssueDetectorTest.kt | 217 ----- .../android/lint/PackageVisibilityDetectorTest.kt | 271 ------ .../lint/RegisterReceiverFlagDetectorTest.kt | 560 ------------- .../lint/aidl/EnforcePermissionDetectorTest.kt | 259 ------ .../aidl/EnforcePermissionHelperDetectorTest.kt | 159 ---- ...impleManualPermissionEnforcementDetectorTest.kt | 371 --------- .../java/com/google/android/lint/aidl/Stubs.kt | 80 -- .../android/lint/parcel/SaferParcelCheckerTest.kt | 823 ------------------ tools/lint/common/Android.bp | 29 + .../main/java/com/google/android/lint/Constants.kt | 40 + .../google/android/lint/PermissionMethodUtils.kt | 36 + .../java/com/google/android/lint/model/Method.kt | 26 + tools/lint/fix/Android.bp | 28 + tools/lint/framework/Android.bp | 58 ++ .../android/lint/AndroidFrameworkIssueRegistry.kt | 62 ++ .../android/lint/CallingIdentityTokenDetector.kt | 648 ++++++++++++++ .../CallingSettingsNonUserGetterMethodsDetector.kt | 83 ++ .../android/lint/PackageVisibilityDetector.kt | 515 ++++++++++++ .../android/lint/PermissionMethodDetector.kt | 199 +++++ .../android/lint/RegisterReceiverFlagDetector.kt | 927 +++++++++++++++++++++ .../google/android/lint/parcel/CallMigrators.kt | 229 +++++ .../java/com/google/android/lint/parcel/Method.kt | 42 + .../android/lint/parcel/SaferParcelChecker.kt | 126 +++ .../lint/CallingIdentityTokenDetectorTest.kt | 867 +++++++++++++++++++ ...ettingsNonUserGetterMethodsIssueDetectorTest.kt | 217 +++++ .../android/lint/PackageVisibilityDetectorTest.kt | 271 ++++++ .../lint/RegisterReceiverFlagDetectorTest.kt | 560 +++++++++++++ .../android/lint/parcel/SaferParcelCheckerTest.kt | 823 ++++++++++++++++++ tools/lint/global/Android.bp | 57 ++ .../android/lint/AndroidGlobalIssueRegistry.kt | 48 ++ .../lint/aidl/AidlImplementationDetector.kt | 52 ++ .../java/com/google/android/lint/aidl/Constants.kt | 73 ++ .../android/lint/aidl/EnforcePermissionDetector.kt | 255 ++++++ .../android/lint/aidl/EnforcePermissionFix.kt | 139 +++ .../lint/aidl/EnforcePermissionHelperDetector.kt | 130 +++ .../android/lint/aidl/EnforcePermissionUtils.kt | 48 ++ .../SimpleManualPermissionEnforcementDetector.kt | 150 ++++ .../lint/aidl/EnforcePermissionDetectorTest.kt | 259 ++++++ .../aidl/EnforcePermissionHelperDetectorTest.kt | 159 ++++ ...impleManualPermissionEnforcementDetectorTest.kt | 371 +++++++++ .../java/com/google/android/lint/aidl/Stubs.kt | 80 ++ 61 files changed, 7607 insertions(+), 7425 deletions(-) delete mode 100644 tools/lint/Android.bp delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt delete mode 100644 tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt delete mode 100644 tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt create mode 100644 tools/lint/common/Android.bp create mode 100644 tools/lint/common/src/main/java/com/google/android/lint/Constants.kt create mode 100644 tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt create mode 100644 tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt create mode 100644 tools/lint/fix/Android.bp create mode 100644 tools/lint/framework/Android.bp create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt create mode 100644 tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt create mode 100644 tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt create mode 100644 tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt create mode 100644 tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt create mode 100644 tools/lint/framework/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt create mode 100644 tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt create mode 100644 tools/lint/global/Android.bp create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt create mode 100644 tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt create mode 100644 tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt create mode 100644 tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt create mode 100644 tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt create mode 100644 tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt (limited to 'tools') diff --git a/tools/lint/Android.bp b/tools/lint/Android.bp deleted file mode 100644 index 96618f413db1..000000000000 --- a/tools/lint/Android.bp +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2021 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -java_library_host { - name: "AndroidFrameworkLintChecker", - srcs: ["checks/src/main/java/**/*.kt"], - plugins: ["auto_service_plugin"], - libs: [ - "auto_service_annotations", - "lint_api", - ], - kotlincflags: ["-Xjvm-default=all"], -} - -java_test_host { - name: "AndroidFrameworkLintCheckerTest", - // TODO(b/239881504): Since this test was written, Android - // Lint was updated, and now includes classes that were - // compiled for java 15. The soong build doesn't support - // java 15 yet, so we can't compile against "lint". Disable - // the test until java 15 is supported. - enabled: false, - srcs: ["checks/src/test/java/**/*.kt"], - static_libs: [ - "AndroidFrameworkLintChecker", - "junit", - "lint", - "lint_tests", - ], - test_options: { - unit_test: true, - }, -} - -python_binary_host { - name: "lint_fix", - main: "fix/lint_fix.py", - srcs: ["fix/lint_fix.py"], -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt deleted file mode 100644 index 413e19717d50..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.aidl.EnforcePermissionDetector -import com.google.android.lint.aidl.EnforcePermissionHelperDetector -import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector -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, - EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, - EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, - SimpleManualPermissionEnforcementDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, - SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, - PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, - RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG, - 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/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt deleted file mode 100644 index 0c375c358e61..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt +++ /dev/null @@ -1,648 +0,0 @@ -/* - * 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 */ - private val tokensMap = mutableMapOf() - - override fun getApplicableUastTypes(): List> = - 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(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 - ) { - 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/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt deleted file mode 100644 index fe567da7c017..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 = 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/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt deleted file mode 100644 index 3d5d01c9b7a0..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.google.android.lint.model.Method - -const val CLASS_STUB = "Stub" -const val CLASS_CONTEXT = "android.content.Context" -const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService" -const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal" - -// Enforce permission APIs -val ENFORCE_PERMISSION_METHODS = listOf( - Method(CLASS_CONTEXT, "checkPermission"), - Method(CLASS_CONTEXT, "checkCallingPermission"), - Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), - Method(CLASS_CONTEXT, "enforcePermission"), - Method(CLASS_CONTEXT, "enforceCallingPermission"), - Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), - Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), - Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") -) - -const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod" -const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName" -const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt deleted file mode 100644 index 48540b1da565..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt +++ /dev/null @@ -1,515 +0,0 @@ -/* - * 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 - * - * Package Visibility that we introduced in Android 11. - *

- * 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, - sinkMethodNodes: List - ) { - 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::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 - ): 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 - ): Boolean { - val startEntry = TraceEntry(apiNode, pkgNameParamIndex) - val traceQueue = LinkedList().apply { add(startEntry) } - val allVisits = mutableSetOf().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? { - 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>): 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/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt deleted file mode 100644 index 1b0f03564c3b..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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.google.android.lint.aidl.hasPermissionMethodAnnotation -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> = - 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 - } - } - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt deleted file mode 100644 index c3e0428316c3..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt +++ /dev/null @@ -1,927 +0,0 @@ -/* - * 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.DataFlowAnalyzer -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.ConstantEvaluator -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.UastLintUtils.Companion.findLastAssignment -import com.android.tools.lint.detector.api.getMethodName -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiVariable -import org.jetbrains.kotlin.psi.psiUtil.parameterIndex -import org.jetbrains.uast.UCallExpression -import org.jetbrains.uast.UElement -import org.jetbrains.uast.UExpression -import org.jetbrains.uast.UParenthesizedExpression -import org.jetbrains.uast.UQualifiedReferenceExpression -import org.jetbrains.uast.getContainingUMethod -import org.jetbrains.uast.isNullLiteral -import org.jetbrains.uast.skipParenthesizedExprDown -import org.jetbrains.uast.tryResolve - -/** - * Detector that identifies `registerReceiver()` calls which are missing the `RECEIVER_EXPORTED` or - * `RECEIVER_NOT_EXPORTED` flags on T+. - * - * TODO: Add API level conditions to better support non-platform code. - * 1. Check if registerReceiver() call is reachable on T+. - * 2. Check if targetSdkVersion is T+. - * - * eg: isWithinVersionCheckConditional(context, node, 31, false) - * eg: isPrecededByVersionCheckExit(context, node, 31) ? - */ -@Suppress("UnstableApiUsage") -class RegisterReceiverFlagDetector : Detector(), SourceCodeScanner { - - override fun getApplicableMethodNames(): List = listOf( - "registerReceiver", - "registerReceiverAsUser", - "registerReceiverForAllUsers" - ) - - private fun checkIsProtectedReceiverAndReturnUnprotectedActions( - filterArg: UExpression, - node: UCallExpression, - evaluator: ConstantEvaluator - ): Pair> { // isProtected, unprotectedActions - val actions = mutableSetOf() - val construction = findIntentFilterConstruction(filterArg, node) - - if (construction == null) return Pair(false, listOf()) - val constructorActionArg = construction.getArgumentForParameter(0) - (constructorActionArg?.let(evaluator::evaluate) as? String)?.let(actions::add) - - val actionCollectorVisitor = - ActionCollectorVisitor(setOf(construction), node, evaluator) - - val parent = node.getContainingUMethod() - parent?.accept(actionCollectorVisitor) - actions.addAll(actionCollectorVisitor.actions) - - // If we failed to evaluate any actions, there will be a null action in the set. - val isProtected = - actions.all(PROTECTED_BROADCASTS::contains) && - !actionCollectorVisitor.intentFilterEscapesScope - val unprotectedActionsList = actions.filterNot(PROTECTED_BROADCASTS::contains) - return Pair(isProtected, unprotectedActionsList) - } - - override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (!context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) return - - // The parameter positions vary across the various registerReceiver*() methods, so rather - // than hardcode them we simply look them up based on the parameter name and type. - val receiverArg = - findArgument(node, method, "android.content.BroadcastReceiver", "receiver") - val filterArg = findArgument(node, method, "android.content.IntentFilter", "filter") - val flagsArg = findArgument(node, method, "int", "flags") - - if (receiverArg == null || receiverArg.isNullLiteral() || filterArg == null) { - return - } - - val evaluator = ConstantEvaluator().allowFieldInitializers() - - val (isProtected, unprotectedActionsList) = - checkIsProtectedReceiverAndReturnUnprotectedActions(filterArg, node, evaluator) - - val flags = evaluator.evaluate(flagsArg) as? Int - - if (!isProtected) { - val actionsList = unprotectedActionsList.joinToString(", ", "", "", -1, "") - val message = "$receiverArg is missing 'RECEIVED_EXPORTED` or 'RECEIVE_NOT_EXPORTED' " + - "flag for unprotected broadcast(s) registered for $actionsList." - if (flagsArg == null) { - context.report( - ISSUE_RECEIVER_EXPORTED_FLAG, node, context.getLocation(node), message) - } else if (flags != null && (flags and RECEIVER_EXPORTED_FLAG_PRESENT_MASK) == 0) { - context.report( - ISSUE_RECEIVER_EXPORTED_FLAG, node, context.getLocation(flagsArg), message) - } - } - - if (DEBUG) { - println(node.asRenderString()) - println("Unprotected Actions: $unprotectedActionsList") - println("Protected: $isProtected") - println("Flags: $flags") - } - } - - /** Finds the first argument of a method that matches the given parameter type and name. */ - private fun findArgument( - node: UCallExpression, - method: PsiMethod, - type: String, - name: String - ): UExpression? { - val psiParameter = method.parameterList.parameters.firstOrNull { - it.type.canonicalText == type && it.name == name - } ?: return null - val argument = node.getArgumentForParameter(psiParameter.parameterIndex()) - return argument?.skipParenthesizedExprDown() - } - - /** - * For the supplied expression (eg. intent filter argument), attempts to find its construction. - * This will be an `IntentFilter()` constructor, an `IntentFilter.create()` call, or `null`. - */ - private fun findIntentFilterConstruction( - expression: UExpression, - node: UCallExpression - ): UCallExpression? { - val resolved = expression.tryResolve() - - if (resolved is PsiVariable) { - val assignment = findLastAssignment(resolved, node) ?: return null - return findIntentFilterConstruction(assignment, node) - } - - if (expression is UParenthesizedExpression) { - return findIntentFilterConstruction(expression.expression, node) - } - - if (expression is UQualifiedReferenceExpression) { - val call = expression.selector as? UCallExpression ?: return null - return if (isReturningContext(call)) { - // eg. filter.apply { addAction("abc") } --> use filter variable. - findIntentFilterConstruction(expression.receiver, node) - } else { - // eg. IntentFilter.create("abc") --> use create("abc") UCallExpression. - findIntentFilterConstruction(call, node) - } - } - - val method = resolved as? PsiMethod ?: return null - return if (isIntentFilterFactoryMethod(method)) { - expression as? UCallExpression - } else { - null - } - } - - private fun isIntentFilterFactoryMethod(method: PsiMethod) = - (method.containingClass?.qualifiedName == "android.content.IntentFilter" && - (method.returnType?.canonicalText == "android.content.IntentFilter" || - method.isConstructor)) - - /** - * Returns true if the given call represents a Kotlin scope function where the return value is - * the context object; see https://kotlinlang.org/docs/scope-functions.html#function-selection. - */ - private fun isReturningContext(node: UCallExpression): Boolean { - val name = getMethodName(node) - if (name == "apply" || name == "also") { - return isScopingFunction(node) - } - return false - } - - /** - * Returns true if the given node appears to be one of the scope functions. Only checks parent - * class; caller should intend that it's actually one of let, with, apply, etc. - */ - private fun isScopingFunction(node: UCallExpression): Boolean { - val called = node.resolve() ?: return true - // See libraries/stdlib/jvm/build/stdlib-declarations.json - return called.containingClass?.qualifiedName == "kotlin.StandardKt__StandardKt" - } - - inner class ActionCollectorVisitor( - start: Collection, - val functionCall: UCallExpression, - val evaluator: ConstantEvaluator, - ) : DataFlowAnalyzer(start) { - private var finished = false - var intentFilterEscapesScope = false; private set - val actions = mutableSetOf() - - override fun argument(call: UCallExpression, reference: UElement) { - // TODO: Remove this temporary fix for DataFlowAnalyzer bug (ag/15787550): - if (reference !in call.valueArguments) return - val methodNames = super@RegisterReceiverFlagDetector.getApplicableMethodNames() - when { - finished -> return - // We've reached the registerReceiver*() call in question. - call == functionCall -> finished = true - // The filter 'intentFilterEscapesScope' to a method which could modify it. - methodNames != null && getMethodName(call)!! !in methodNames -> - intentFilterEscapesScope = true - } - } - - // Fixed in b/199163915: DataFlowAnalyzer doesn't call this for Kotlin properties. - override fun field(field: UElement) { - if (!finished) intentFilterEscapesScope = true - } - - override fun receiver(call: UCallExpression) { - if (!finished && getMethodName(call) == "addAction") { - val actionArg = call.getArgumentForParameter(0) - if (actionArg != null) { - val action = evaluator.evaluate(actionArg) as? String - if (action != null) actions.add(action) - } - } - } - } - - companion object { - const val DEBUG = false - - private const val RECEIVER_EXPORTED = 0x2 - private const val RECEIVER_NOT_EXPORTED = 0x4 - private const val RECEIVER_EXPORTED_FLAG_PRESENT_MASK = - RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED - - @JvmField - val ISSUE_RECEIVER_EXPORTED_FLAG: Issue = Issue.create( - id = "UnspecifiedRegisterReceiverFlag", - briefDescription = "Missing `registerReceiver()` exported flag", - explanation = """ - Apps targeting Android T (SDK 33) and higher must specify either `RECEIVER_EXPORTED` \ - or `RECEIVER_NOT_EXPORTED` when registering a broadcast receiver, unless the \ - receiver is only registered for protected system broadcast actions. - """, - category = Category.SECURITY, - priority = 5, - severity = Severity.WARNING, - implementation = Implementation( - RegisterReceiverFlagDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - - val PROTECTED_BROADCASTS = listOf( - "android.intent.action.SCREEN_OFF", - "android.intent.action.SCREEN_ON", - "android.intent.action.USER_PRESENT", - "android.intent.action.TIME_SET", - "android.intent.action.TIME_TICK", - "android.intent.action.TIMEZONE_CHANGED", - "android.intent.action.DATE_CHANGED", - "android.intent.action.PRE_BOOT_COMPLETED", - "android.intent.action.BOOT_COMPLETED", - "android.intent.action.PACKAGE_INSTALL", - "android.intent.action.PACKAGE_ADDED", - "android.intent.action.PACKAGE_REPLACED", - "android.intent.action.MY_PACKAGE_REPLACED", - "android.intent.action.PACKAGE_REMOVED", - "android.intent.action.PACKAGE_REMOVED_INTERNAL", - "android.intent.action.PACKAGE_FULLY_REMOVED", - "android.intent.action.PACKAGE_CHANGED", - "android.intent.action.PACKAGE_FULLY_LOADED", - "android.intent.action.PACKAGE_ENABLE_ROLLBACK", - "android.intent.action.CANCEL_ENABLE_ROLLBACK", - "android.intent.action.ROLLBACK_COMMITTED", - "android.intent.action.PACKAGE_RESTARTED", - "android.intent.action.PACKAGE_DATA_CLEARED", - "android.intent.action.PACKAGE_FIRST_LAUNCH", - "android.intent.action.PACKAGE_NEEDS_INTEGRITY_VERIFICATION", - "android.intent.action.PACKAGE_NEEDS_VERIFICATION", - "android.intent.action.PACKAGE_VERIFIED", - "android.intent.action.PACKAGES_SUSPENDED", - "android.intent.action.PACKAGES_UNSUSPENDED", - "android.intent.action.PACKAGES_SUSPENSION_CHANGED", - "android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY", - "android.intent.action.DISTRACTING_PACKAGES_CHANGED", - "android.intent.action.ACTION_PREFERRED_ACTIVITY_CHANGED", - "android.intent.action.UID_REMOVED", - "android.intent.action.QUERY_PACKAGE_RESTART", - "android.intent.action.CONFIGURATION_CHANGED", - "android.intent.action.SPLIT_CONFIGURATION_CHANGED", - "android.intent.action.LOCALE_CHANGED", - "android.intent.action.APPLICATION_LOCALE_CHANGED", - "android.intent.action.BATTERY_CHANGED", - "android.intent.action.BATTERY_LEVEL_CHANGED", - "android.intent.action.BATTERY_LOW", - "android.intent.action.BATTERY_OKAY", - "android.intent.action.ACTION_POWER_CONNECTED", - "android.intent.action.ACTION_POWER_DISCONNECTED", - "android.intent.action.ACTION_SHUTDOWN", - "android.intent.action.CHARGING", - "android.intent.action.DISCHARGING", - "android.intent.action.DEVICE_STORAGE_LOW", - "android.intent.action.DEVICE_STORAGE_OK", - "android.intent.action.DEVICE_STORAGE_FULL", - "android.intent.action.DEVICE_STORAGE_NOT_FULL", - "android.intent.action.NEW_OUTGOING_CALL", - "android.intent.action.REBOOT", - "android.intent.action.DOCK_EVENT", - "android.intent.action.THERMAL_EVENT", - "android.intent.action.MASTER_CLEAR_NOTIFICATION", - "android.intent.action.USER_ADDED", - "android.intent.action.USER_REMOVED", - "android.intent.action.USER_STARTING", - "android.intent.action.USER_STARTED", - "android.intent.action.USER_STOPPING", - "android.intent.action.USER_STOPPED", - "android.intent.action.USER_BACKGROUND", - "android.intent.action.USER_FOREGROUND", - "android.intent.action.USER_SWITCHED", - "android.intent.action.USER_INITIALIZE", - "android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION", - "android.intent.action.DOMAINS_NEED_VERIFICATION", - "android.intent.action.OVERLAY_ADDED", - "android.intent.action.OVERLAY_CHANGED", - "android.intent.action.OVERLAY_REMOVED", - "android.intent.action.OVERLAY_PRIORITY_CHANGED", - "android.intent.action.MY_PACKAGE_SUSPENDED", - "android.intent.action.MY_PACKAGE_UNSUSPENDED", - "android.os.action.POWER_SAVE_MODE_CHANGED", - "android.os.action.DEVICE_IDLE_MODE_CHANGED", - "android.os.action.POWER_SAVE_WHITELIST_CHANGED", - "android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED", - "android.os.action.POWER_SAVE_MODE_CHANGED_INTERNAL", - "android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED", - "android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED", - "android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED", - "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL", - "android.app.action.ENTER_CAR_MODE", - "android.app.action.EXIT_CAR_MODE", - "android.app.action.ENTER_CAR_MODE_PRIORITIZED", - "android.app.action.EXIT_CAR_MODE_PRIORITIZED", - "android.app.action.ENTER_DESK_MODE", - "android.app.action.EXIT_DESK_MODE", - "android.app.action.NEXT_ALARM_CLOCK_CHANGED", - "android.app.action.USER_ADDED", - "android.app.action.USER_REMOVED", - "android.app.action.USER_STARTED", - "android.app.action.USER_STOPPED", - "android.app.action.USER_SWITCHED", - "android.app.action.BUGREPORT_SHARING_DECLINED", - "android.app.action.BUGREPORT_FAILED", - "android.app.action.BUGREPORT_SHARE", - "android.app.action.SHOW_DEVICE_MONITORING_DIALOG", - "android.intent.action.PENDING_INCIDENT_REPORTS_CHANGED", - "android.intent.action.INCIDENT_REPORT_READY", - "android.appwidget.action.APPWIDGET_UPDATE_OPTIONS", - "android.appwidget.action.APPWIDGET_DELETED", - "android.appwidget.action.APPWIDGET_DISABLED", - "android.appwidget.action.APPWIDGET_ENABLED", - "android.appwidget.action.APPWIDGET_HOST_RESTORED", - "android.appwidget.action.APPWIDGET_RESTORED", - "android.appwidget.action.APPWIDGET_ENABLE_AND_UPDATE", - "android.os.action.SETTING_RESTORED", - "android.app.backup.intent.CLEAR", - "android.app.backup.intent.INIT", - "android.bluetooth.intent.DISCOVERABLE_TIMEOUT", - "android.bluetooth.adapter.action.STATE_CHANGED", - "android.bluetooth.adapter.action.SCAN_MODE_CHANGED", - "android.bluetooth.adapter.action.DISCOVERY_STARTED", - "android.bluetooth.adapter.action.DISCOVERY_FINISHED", - "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED", - "android.bluetooth.adapter.action.BLUETOOTH_ADDRESS_CHANGED", - "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.device.action.UUID", - "android.bluetooth.device.action.MAS_INSTANCE", - "android.bluetooth.device.action.ALIAS_CHANGED", - "android.bluetooth.device.action.FOUND", - "android.bluetooth.device.action.CLASS_CHANGED", - "android.bluetooth.device.action.ACL_CONNECTED", - "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED", - "android.bluetooth.device.action.ACL_DISCONNECTED", - "android.bluetooth.device.action.NAME_CHANGED", - "android.bluetooth.device.action.BOND_STATE_CHANGED", - "android.bluetooth.device.action.NAME_FAILED", - "android.bluetooth.device.action.PAIRING_REQUEST", - "android.bluetooth.device.action.PAIRING_CANCEL", - "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY", - "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL", - "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST", - "android.bluetooth.device.action.SDP_RECORD", - "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED", - "android.bluetooth.devicepicker.action.LAUNCH", - "android.bluetooth.devicepicker.action.DEVICE_SELECTED", - "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED", - "android.bluetooth.action.CSIS_DEVICE_AVAILABLE", - "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE", - "android.bluetooth.mapmce.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED", - "android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY", - "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY", - "android.bluetooth.mapmce.profile.action.MESSAGE_READ_STATUS_CHANGED", - "android.bluetooth.mapmce.profile.action.MESSAGE_DELETED_STATUS_CHANGED", - "android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED", - "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED", - "android.bluetooth.action.LE_AUDIO_CONF_CHANGED", - "android.bluetooth.action.LE_AUDIO_GROUP_NODE_STATUS_CHANGED", - "android.bluetooth.action.LE_AUDIO_GROUP_STATUS_CHANGED", - "android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.pbapclient.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED", - "android.btopp.intent.action.INCOMING_FILE_NOTIFICATION", - "android.btopp.intent.action.USER_CONFIRMATION_TIMEOUT", - "android.btopp.intent.action.LIST", - "android.btopp.intent.action.OPEN_OUTBOUND", - "android.btopp.intent.action.HIDE_COMPLETE", - "android.btopp.intent.action.CONFIRM", - "android.btopp.intent.action.HIDE", - "android.btopp.intent.action.RETRY", - "android.btopp.intent.action.OPEN", - "android.btopp.intent.action.OPEN_INBOUND", - "android.btopp.intent.action.TRANSFER_COMPLETE", - "android.btopp.intent.action.ACCEPT", - "android.btopp.intent.action.DECLINE", - "com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN", - "com.android.bluetooth.pbap.authchall", - "com.android.bluetooth.pbap.userconfirmtimeout", - "com.android.bluetooth.pbap.authresponse", - "com.android.bluetooth.pbap.authcancelled", - "com.android.bluetooth.sap.USER_CONFIRM_TIMEOUT", - "com.android.bluetooth.sap.action.DISCONNECT_ACTION", - "android.hardware.display.action.WIFI_DISPLAY_STATUS_CHANGED", - "android.hardware.usb.action.USB_STATE", - "android.hardware.usb.action.USB_PORT_CHANGED", - "android.hardware.usb.action.USB_ACCESSORY_ATTACHED", - "android.hardware.usb.action.USB_ACCESSORY_DETACHED", - "android.hardware.usb.action.USB_ACCESSORY_HANDSHAKE", - "android.hardware.usb.action.USB_DEVICE_ATTACHED", - "android.hardware.usb.action.USB_DEVICE_DETACHED", - "android.intent.action.HEADSET_PLUG", - "android.media.action.HDMI_AUDIO_PLUG", - "android.media.action.MICROPHONE_MUTE_CHANGED", - "android.media.action.SPEAKERPHONE_STATE_CHANGED", - "android.media.AUDIO_BECOMING_NOISY", - "android.media.RINGER_MODE_CHANGED", - "android.media.VIBRATE_SETTING_CHANGED", - "android.media.VOLUME_CHANGED_ACTION", - "android.media.MASTER_VOLUME_CHANGED_ACTION", - "android.media.MASTER_MUTE_CHANGED_ACTION", - "android.media.MASTER_MONO_CHANGED_ACTION", - "android.media.MASTER_BALANCE_CHANGED_ACTION", - "android.media.SCO_AUDIO_STATE_CHANGED", - "android.media.ACTION_SCO_AUDIO_STATE_UPDATED", - "android.intent.action.MEDIA_REMOVED", - "android.intent.action.MEDIA_UNMOUNTED", - "android.intent.action.MEDIA_CHECKING", - "android.intent.action.MEDIA_NOFS", - "android.intent.action.MEDIA_MOUNTED", - "android.intent.action.MEDIA_SHARED", - "android.intent.action.MEDIA_UNSHARED", - "android.intent.action.MEDIA_BAD_REMOVAL", - "android.intent.action.MEDIA_UNMOUNTABLE", - "android.intent.action.MEDIA_EJECT", - "android.net.conn.CAPTIVE_PORTAL", - "android.net.conn.CONNECTIVITY_CHANGE", - "android.net.conn.CONNECTIVITY_CHANGE_IMMEDIATE", - "android.net.conn.DATA_ACTIVITY_CHANGE", - "android.net.conn.RESTRICT_BACKGROUND_CHANGED", - "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED", - "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED", - "android.net.nsd.STATE_CHANGED", - "android.se.omapi.action.SECURE_ELEMENT_STATE_CHANGED", - "android.nfc.action.ADAPTER_STATE_CHANGED", - "android.nfc.action.PREFERRED_PAYMENT_CHANGED", - "android.nfc.action.TRANSACTION_DETECTED", - "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC", - "com.android.nfc.action.LLCP_UP", - "com.android.nfc.action.LLCP_DOWN", - "com.android.nfc.cardemulation.action.CLOSE_TAP_DIALOG", - "com.android.nfc.handover.action.ALLOW_CONNECT", - "com.android.nfc.handover.action.DENY_CONNECT", - "com.android.nfc.handover.action.TIMEOUT_CONNECT", - "com.android.nfc_extras.action.RF_FIELD_ON_DETECTED", - "com.android.nfc_extras.action.RF_FIELD_OFF_DETECTED", - "com.android.nfc_extras.action.AID_SELECTED", - "android.btopp.intent.action.WHITELIST_DEVICE", - "android.btopp.intent.action.STOP_HANDOVER_TRANSFER", - "android.nfc.handover.intent.action.HANDOVER_SEND", - "android.nfc.handover.intent.action.HANDOVER_SEND_MULTIPLE", - "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER", - "android.net.action.CLEAR_DNS_CACHE", - "android.intent.action.PROXY_CHANGE", - "android.os.UpdateLock.UPDATE_LOCK_CHANGED", - "android.intent.action.DREAMING_STARTED", - "android.intent.action.DREAMING_STOPPED", - "android.intent.action.ANY_DATA_STATE", - "com.android.server.stats.action.TRIGGER_COLLECTION", - "com.android.server.WifiManager.action.START_SCAN", - "com.android.server.WifiManager.action.START_PNO", - "com.android.server.WifiManager.action.DELAYED_DRIVER_STOP", - "com.android.server.WifiManager.action.DEVICE_IDLE", - "com.android.server.action.REMOTE_BUGREPORT_SHARING_ACCEPTED", - "com.android.server.action.REMOTE_BUGREPORT_SHARING_DECLINED", - "com.android.internal.action.EUICC_FACTORY_RESET", - "com.android.server.usb.ACTION_OPEN_IN_APPS", - "com.android.server.am.DELETE_DUMPHEAP", - "com.android.server.net.action.SNOOZE_WARNING", - "com.android.server.net.action.SNOOZE_RAPID", - "com.android.server.wifi.ACTION_SHOW_SET_RANDOMIZATION_DETAILS", - "com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_APP", - "com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_APP", - "com.android.server.wifi.action.NetworkSuggestion.USER_DISMISSED", - "com.android.server.wifi.action.CarrierNetwork.USER_ALLOWED_CARRIER", - "com.android.server.wifi.action.CarrierNetwork.USER_DISALLOWED_CARRIER", - "com.android.server.wifi.action.CarrierNetwork.USER_DISMISSED", - "com.android.server.wifi.ConnectToNetworkNotification.USER_DISMISSED_NOTIFICATION", - "com.android.server.wifi.ConnectToNetworkNotification.CONNECT_TO_NETWORK", - "com.android.server.wifi.ConnectToNetworkNotification.PICK_WIFI_NETWORK", - "com.android.server.wifi.ConnectToNetworkNotification.PICK_NETWORK_AFTER_FAILURE", - "com.android.server.wifi.wakeup.DISMISS_NOTIFICATION", - "com.android.server.wifi.wakeup.OPEN_WIFI_PREFERENCES", - "com.android.server.wifi.wakeup.OPEN_WIFI_SETTINGS", - "com.android.server.wifi.wakeup.TURN_OFF_WIFI_WAKE", - "android.net.wifi.WIFI_STATE_CHANGED", - "android.net.wifi.WIFI_AP_STATE_CHANGED", - "android.net.wifi.WIFI_CREDENTIAL_CHANGED", - "android.net.wifi.aware.action.WIFI_AWARE_STATE_CHANGED", - "android.net.wifi.aware.action.WIFI_AWARE_RESOURCE_CHANGED", - "android.net.wifi.rtt.action.WIFI_RTT_STATE_CHANGED", - "android.net.wifi.SCAN_RESULTS", - "android.net.wifi.RSSI_CHANGED", - "android.net.wifi.STATE_CHANGE", - "android.net.wifi.LINK_CONFIGURATION_CHANGED", - "android.net.wifi.CONFIGURED_NETWORKS_CHANGE", - "android.net.wifi.action.NETWORK_SETTINGS_RESET", - "android.net.wifi.action.PASSPOINT_DEAUTH_IMMINENT", - "android.net.wifi.action.PASSPOINT_ICON", - "android.net.wifi.action.PASSPOINT_OSU_PROVIDERS_LIST", - "android.net.wifi.action.PASSPOINT_SUBSCRIPTION_REMEDIATION", - "android.net.wifi.action.PASSPOINT_LAUNCH_OSU_VIEW", - "android.net.wifi.action.REFRESH_USER_PROVISIONING", - "android.net.wifi.action.WIFI_NETWORK_SUGGESTION_POST_CONNECTION", - "android.net.wifi.action.WIFI_SCAN_AVAILABILITY_CHANGED", - "android.net.wifi.supplicant.CONNECTION_CHANGE", - "android.net.wifi.supplicant.STATE_CHANGE", - "android.net.wifi.p2p.STATE_CHANGED", - "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE", - "android.net.wifi.p2p.THIS_DEVICE_CHANGED", - "android.net.wifi.p2p.PEERS_CHANGED", - "android.net.wifi.p2p.CONNECTION_STATE_CHANGE", - "android.net.wifi.p2p.action.WIFI_P2P_PERSISTENT_GROUPS_CHANGED", - "android.net.conn.TETHER_STATE_CHANGED", - "android.net.conn.INET_CONDITION_ACTION", - "android.net.conn.NETWORK_CONDITIONS_MEASURED", - "android.net.scoring.SCORE_NETWORKS", - "android.net.scoring.SCORER_CHANGED", - "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE", - "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE", - "android.intent.action.AIRPLANE_MODE", - "android.intent.action.ADVANCED_SETTINGS", - "android.intent.action.APPLICATION_RESTRICTIONS_CHANGED", - "com.android.server.adb.WIRELESS_DEBUG_PAIRED_DEVICES", - "com.android.server.adb.WIRELESS_DEBUG_PAIRING_RESULT", - "com.android.server.adb.WIRELESS_DEBUG_STATUS", - "android.intent.action.ACTION_IDLE_MAINTENANCE_START", - "android.intent.action.ACTION_IDLE_MAINTENANCE_END", - "com.android.server.ACTION_TRIGGER_IDLE", - "android.intent.action.HDMI_PLUGGED", - "android.intent.action.PHONE_STATE", - "android.intent.action.SUB_DEFAULT_CHANGED", - "android.location.PROVIDERS_CHANGED", - "android.location.MODE_CHANGED", - "android.location.action.GNSS_CAPABILITIES_CHANGED", - "android.net.proxy.PAC_REFRESH", - "android.telecom.action.DEFAULT_DIALER_CHANGED", - "android.provider.action.DEFAULT_SMS_PACKAGE_CHANGED", - "android.provider.action.SMS_MMS_DB_CREATED", - "android.provider.action.SMS_MMS_DB_LOST", - "android.intent.action.CONTENT_CHANGED", - "android.provider.Telephony.MMS_DOWNLOADED", - "android.content.action.PERMISSION_RESPONSE_RECEIVED", - "android.content.action.REQUEST_PERMISSION", - "android.nfc.handover.intent.action.HANDOVER_STARTED", - "android.nfc.handover.intent.action.TRANSFER_DONE", - "android.nfc.handover.intent.action.TRANSFER_PROGRESS", - "android.nfc.handover.intent.action.TRANSFER_DONE", - "android.intent.action.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED", - "android.intent.action.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED", - "android.intent.action.ACTION_SET_RADIO_CAPABILITY_DONE", - "android.intent.action.ACTION_SET_RADIO_CAPABILITY_FAILED", - "android.internal.policy.action.BURN_IN_PROTECTION", - "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED", - "android.app.action.RESET_PROTECTION_POLICY_CHANGED", - "android.app.action.DEVICE_OWNER_CHANGED", - "android.app.action.MANAGED_USER_CREATED", - "android.intent.action.ANR", - "android.intent.action.CALL", - "android.intent.action.CALL_PRIVILEGED", - "android.intent.action.DROPBOX_ENTRY_ADDED", - "android.intent.action.INPUT_METHOD_CHANGED", - "android.intent.action.internal_sim_state_changed", - "android.intent.action.LOCKED_BOOT_COMPLETED", - "android.intent.action.PRECISE_CALL_STATE", - "android.intent.action.SUBSCRIPTION_PHONE_STATE", - "android.intent.action.USER_INFO_CHANGED", - "android.intent.action.USER_UNLOCKED", - "android.intent.action.WALLPAPER_CHANGED", - "android.app.action.DEVICE_POLICY_MANAGER_STATE_CHANGED", - "android.app.action.CHOOSE_PRIVATE_KEY_ALIAS", - "android.app.action.DEVICE_ADMIN_DISABLED", - "android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED", - "android.app.action.DEVICE_ADMIN_ENABLED", - "android.app.action.LOCK_TASK_ENTERING", - "android.app.action.LOCK_TASK_EXITING", - "android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE", - "android.app.action.ACTION_PASSWORD_CHANGED", - "android.app.action.ACTION_PASSWORD_EXPIRING", - "android.app.action.ACTION_PASSWORD_FAILED", - "android.app.action.ACTION_PASSWORD_SUCCEEDED", - "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION", - "com.android.server.ACTION_PROFILE_OFF_DEADLINE", - "com.android.server.ACTION_TURN_PROFILE_ON_NOTIFICATION", - "android.intent.action.MANAGED_PROFILE_ADDED", - "android.intent.action.MANAGED_PROFILE_UNLOCKED", - "android.intent.action.MANAGED_PROFILE_REMOVED", - "android.app.action.MANAGED_PROFILE_PROVISIONED", - "android.bluetooth.adapter.action.BLE_STATE_CHANGED", - "com.android.bluetooth.map.USER_CONFIRM_TIMEOUT", - "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT", - "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY", - "android.content.jobscheduler.JOB_DELAY_EXPIRED", - "android.content.syncmanager.SYNC_ALARM", - "android.media.INTERNAL_RINGER_MODE_CHANGED_ACTION", - "android.media.STREAM_DEVICES_CHANGED_ACTION", - "android.media.STREAM_MUTE_CHANGED_ACTION", - "android.net.sip.SIP_SERVICE_UP", - "android.nfc.action.ADAPTER_STATE_CHANGED", - "android.os.action.CHARGING", - "android.os.action.DISCHARGING", - "android.search.action.SEARCHABLES_CHANGED", - "android.security.STORAGE_CHANGED", - "android.security.action.TRUST_STORE_CHANGED", - "android.security.action.KEYCHAIN_CHANGED", - "android.security.action.KEY_ACCESS_CHANGED", - "android.telecom.action.NUISANCE_CALL_STATUS_CHANGED", - "android.telecom.action.PHONE_ACCOUNT_REGISTERED", - "android.telecom.action.PHONE_ACCOUNT_UNREGISTERED", - "android.telecom.action.POST_CALL", - "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION", - "android.telephony.action.CARRIER_CONFIG_CHANGED", - "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED", - "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED", - "android.telephony.action.SECRET_CODE", - "android.telephony.action.SHOW_VOICEMAIL_NOTIFICATION", - "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED", - "com.android.bluetooth.btservice.action.ALARM_WAKEUP", - "com.android.server.action.NETWORK_STATS_POLL", - "com.android.server.action.NETWORK_STATS_UPDATED", - "com.android.server.timedetector.NetworkTimeUpdateService.action.POLL", - "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY", - "com.android.settings.location.MODE_CHANGING", - "com.android.settings.bluetooth.ACTION_DISMISS_PAIRING", - "com.android.settings.network.DELETE_SUBSCRIPTION", - "com.android.settings.network.SWITCH_TO_SUBSCRIPTION", - "com.android.settings.wifi.action.NETWORK_REQUEST", - "NotificationManagerService.TIMEOUT", - "NotificationHistoryDatabase.CLEANUP", - "ScheduleConditionProvider.EVALUATE", - "EventConditionProvider.EVALUATE", - "SnoozeHelper.EVALUATE", - "wifi_scan_available", - "action.cne.started", - "android.content.jobscheduler.JOB_DEADLINE_EXPIRED", - "android.intent.action.ACTION_UNSOL_RESPONSE_OEM_HOOK_RAW", - "android.net.conn.CONNECTIVITY_CHANGE_SUPL", - "android.os.action.LIGHT_DEVICE_IDLE_MODE_CHANGED", - "android.os.storage.action.VOLUME_STATE_CHANGED", - "android.os.storage.action.DISK_SCANNED", - "com.android.server.action.UPDATE_TWILIGHT_STATE", - "com.android.server.action.RESET_TWILIGHT_AUTO", - "com.android.server.device_idle.STEP_IDLE_STATE", - "com.android.server.device_idle.STEP_LIGHT_IDLE_STATE", - "com.android.server.Wifi.action.TOGGLE_PNO", - "intent.action.ACTION_RF_BAND_INFO", - "android.intent.action.MEDIA_RESOURCE_GRANTED", - "android.app.action.NETWORK_LOGS_AVAILABLE", - "android.app.action.SECURITY_LOGS_AVAILABLE", - "android.app.action.COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED", - "android.app.action.INTERRUPTION_FILTER_CHANGED", - "android.app.action.INTERRUPTION_FILTER_CHANGED_INTERNAL", - "android.app.action.NOTIFICATION_POLICY_CHANGED", - "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED", - "android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED", - "android.os.action.ACTION_EFFECTS_SUPPRESSOR_CHANGED", - "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED", - "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED", - "android.app.action.NOTIFICATION_LISTENER_ENABLED_CHANGED", - "android.app.action.APP_BLOCK_STATE_CHANGED", - "android.permission.GET_APP_GRANTED_URI_PERMISSIONS", - "android.permission.CLEAR_APP_GRANTED_URI_PERMISSIONS", - "android.intent.action.DYNAMIC_SENSOR_CHANGED", - "android.accounts.LOGIN_ACCOUNTS_CHANGED", - "android.accounts.action.ACCOUNT_REMOVED", - "android.accounts.action.VISIBLE_ACCOUNTS_CHANGED", - "com.android.sync.SYNC_CONN_STATUS_CHANGED", - "android.net.sip.action.SIP_INCOMING_CALL", - "com.android.phone.SIP_ADD_PHONE", - "android.net.sip.action.SIP_REMOVE_PROFILE", - "android.net.sip.action.SIP_SERVICE_UP", - "android.net.sip.action.SIP_CALL_OPTION_CHANGED", - "android.net.sip.action.START_SIP", - "android.bluetooth.adapter.action.BLE_ACL_CONNECTED", - "android.bluetooth.adapter.action.BLE_ACL_DISCONNECTED", - "android.bluetooth.input.profile.action.HANDSHAKE", - "android.bluetooth.input.profile.action.REPORT", - "android.intent.action.TWILIGHT_CHANGED", - "com.android.server.fingerprint.ACTION_LOCKOUT_RESET", - "android.net.wifi.PASSPOINT_ICON_RECEIVED", - "com.android.server.notification.CountdownConditionProvider", - "android.server.notification.action.ENABLE_NAS", - "android.server.notification.action.DISABLE_NAS", - "android.server.notification.action.LEARNMORE_NAS", - "com.android.internal.location.ALARM_WAKEUP", - "com.android.internal.location.ALARM_TIMEOUT", - "android.intent.action.GLOBAL_BUTTON", - "android.intent.action.MANAGED_PROFILE_AVAILABLE", - "android.intent.action.MANAGED_PROFILE_UNAVAILABLE", - "com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK", - "android.intent.action.PROFILE_ACCESSIBLE", - "android.intent.action.PROFILE_INACCESSIBLE", - "com.android.server.retaildemo.ACTION_RESET_DEMO", - "android.intent.action.DEVICE_LOCKED_CHANGED", - "com.android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED", - "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED", - "com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION", - "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED", - "android.content.pm.action.SESSION_COMMITTED", - "android.os.action.USER_RESTRICTIONS_CHANGED", - "android.media.tv.action.PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT", - "android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED", - "android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED", - "android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED", - "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER", - "com.android.intent.action.timezone.RULES_UPDATE_OPERATION", - "com.android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK", - "android.intent.action.GET_RESTRICTION_ENTRIES", - "android.telephony.euicc.action.OTA_STATUS_CHANGED", - "android.app.action.PROFILE_OWNER_CHANGED", - "android.app.action.TRANSFER_OWNERSHIP_COMPLETE", - "android.app.action.AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE", - "android.app.action.STATSD_STARTED", - "com.android.server.biometrics.fingerprint.ACTION_LOCKOUT_RESET", - "com.android.server.biometrics.face.ACTION_LOCKOUT_RESET", - "android.intent.action.DOCK_IDLE", - "android.intent.action.DOCK_ACTIVE", - "android.content.pm.action.SESSION_UPDATED", - "android.settings.action.GRAYSCALE_CHANGED", - "com.android.server.jobscheduler.GARAGE_MODE_ON", - "com.android.server.jobscheduler.GARAGE_MODE_OFF", - "com.android.server.jobscheduler.FORCE_IDLE", - "com.android.server.jobscheduler.UNFORCE_IDLE", - "android.provider.action.DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL", - "android.intent.action.DEVICE_CUSTOMIZATION_READY", - "android.app.action.RESET_PROTECTION_POLICY_CHANGED", - "com.android.internal.intent.action.BUGREPORT_REQUESTED", - "android.scheduling.action.REBOOT_READY", - "android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED", - "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED", - "android.app.action.SHOW_NEW_USER_DISCLAIMER", - "android.telecom.action.CURRENT_TTY_MODE_CHANGED", - "android.intent.action.SERVICE_STATE", - "android.intent.action.RADIO_TECHNOLOGY", - "android.intent.action.EMERGENCY_CALLBACK_MODE_CHANGED", - "android.intent.action.EMERGENCY_CALL_STATE_CHANGED", - "android.intent.action.SIG_STR", - "android.intent.action.ANY_DATA_STATE", - "android.intent.action.DATA_STALL_DETECTED", - "android.intent.action.SIM_STATE_CHANGED", - "android.intent.action.USER_ACTIVITY_NOTIFICATION", - "android.telephony.action.SHOW_NOTICE_ECM_BLOCK_OTHERS", - "android.intent.action.ACTION_MDN_STATE_CHANGED", - "android.telephony.action.SERVICE_PROVIDERS_UPDATED", - "android.provider.Telephony.SIM_FULL", - "com.android.internal.telephony.carrier_key_download_alarm", - "com.android.internal.telephony.data-restart-trysetup", - "com.android.internal.telephony.data-stall", - "com.android.internal.telephony.provisioning_apn_alarm", - "android.intent.action.DATA_SMS_RECEIVED", - "android.provider.Telephony.SMS_RECEIVED", - "android.provider.Telephony.SMS_DELIVER", - "android.provider.Telephony.SMS_REJECTED", - "android.provider.Telephony.WAP_PUSH_DELIVER", - "android.provider.Telephony.WAP_PUSH_RECEIVED", - "android.provider.Telephony.SMS_CB_RECEIVED", - "android.provider.action.SMS_EMERGENCY_CB_RECEIVED", - "android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED", - "android.provider.Telephony.SECRET_CODE", - "com.android.internal.stk.command", - "com.android.internal.stk.session_end", - "com.android.internal.stk.icc_status_change", - "com.android.internal.stk.alpha_notify", - "com.android.internal.telephony.CARRIER_SIGNAL_REDIRECTED", - "com.android.internal.telephony.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED", - "com.android.internal.telephony.CARRIER_SIGNAL_PCO_VALUE", - "com.android.internal.telephony.CARRIER_SIGNAL_RESET", - "com.android.internal.telephony.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE", - "com.android.internal.telephony.PROVISION", - "com.android.internal.telephony.ACTION_LINE1_NUMBER_ERROR_DETECTED", - "com.android.internal.provider.action.VOICEMAIL_SMS_RECEIVED", - "com.android.intent.isim_refresh", - "com.android.ims.ACTION_RCS_SERVICE_AVAILABLE", - "com.android.ims.ACTION_RCS_SERVICE_UNAVAILABLE", - "com.android.ims.ACTION_RCS_SERVICE_DIED", - "com.android.ims.ACTION_PRESENCE_CHANGED", - "com.android.ims.ACTION_PUBLISH_STATUS_CHANGED", - "com.android.ims.IMS_SERVICE_UP", - "com.android.ims.IMS_SERVICE_DOWN", - "com.android.ims.IMS_INCOMING_CALL", - "com.android.ims.internal.uce.UCE_SERVICE_UP", - "com.android.ims.internal.uce.UCE_SERVICE_DOWN", - "com.android.imsconnection.DISCONNECTED", - "com.android.intent.action.IMS_FEATURE_CHANGED", - "com.android.intent.action.IMS_CONFIG_CHANGED", - "android.telephony.ims.action.WFC_IMS_REGISTRATION_ERROR", - "com.android.phone.vvm.omtp.sms.REQUEST_SENT", - "com.android.phone.vvm.ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT", - "com.android.internal.telephony.CARRIER_VVM_PACKAGE_INSTALLED", - "com.android.cellbroadcastreceiver.GET_LATEST_CB_AREA_INFO", - "com.android.internal.telephony.ACTION_CARRIER_CERTIFICATE_DOWNLOAD", - "com.android.internal.telephony.action.COUNTRY_OVERRIDE", - "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP", - "com.android.internal.telephony.ACTION_TEST_OVERRIDE_CARRIER_ID", - "android.telephony.action.SIM_CARD_STATE_CHANGED", - "android.telephony.action.SIM_APPLICATION_STATE_CHANGED", - "android.telephony.action.SIM_SLOT_STATUS_CHANGED", - "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED", - "android.telephony.action.SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED", - "android.telephony.action.TOGGLE_PROVISION", - "android.telephony.action.NETWORK_COUNTRY_CHANGED", - "android.telephony.action.PRIMARY_SUBSCRIPTION_LIST_CHANGED", - "android.telephony.action.MULTI_SIM_CONFIG_CHANGED", - "android.telephony.action.CARRIER_SIGNAL_RESET", - "android.telephony.action.CARRIER_SIGNAL_PCO_VALUE", - "android.telephony.action.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE", - "android.telephony.action.CARRIER_SIGNAL_REDIRECTED", - "android.telephony.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED", - "com.android.phone.settings.CARRIER_PROVISIONING", - "com.android.phone.settings.TRIGGER_CARRIER_PROVISIONING", - "com.android.internal.telephony.ACTION_VOWIFI_ENABLED", - "android.telephony.action.ANOMALY_REPORTED", - "android.intent.action.SUBSCRIPTION_INFO_RECORD_ADDED", - "android.intent.action.ACTION_MANAGED_ROAMING_IND", - "android.telephony.ims.action.RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE", - "android.safetycenter.action.REFRESH_SAFETY_SOURCES", - "android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED", - "android.app.action.DEVICE_POLICY_RESOURCE_UPDATED", - "android.intent.action.SHOW_FOREGROUND_SERVICE_MANAGER", - "android.service.autofill.action.DELAYED_FILL", - "android.app.action.PROVISIONING_COMPLETED", - "android.app.action.LOST_MODE_LOCATION_UPDATE", - "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED", - "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT", - "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED", - "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED", - "android.bluetooth.headsetclient.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.headsetclient.profile.action.AUDIO_STATE_CHANGED", - "android.bluetooth.headsetclient.profile.action.AG_EVENT", - "android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED", - "android.bluetooth.headsetclient.profile.action.RESULT", - "android.bluetooth.headsetclient.profile.action.LAST_VTAG", - "android.bluetooth.headsetclient.profile.action.NETWORK_SERVICE_STATE_CHANGED", - "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.hearingaid.profile.action.PLAYING_STATE_CHANGED", - "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED", - "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.a2dp.profile.action.ACTIVE_DEVICE_CHANGED", - "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED", - "android.bluetooth.a2dp.profile.action.CODEC_CONFIG_CHANGED", - "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.a2dp-sink.profile.action.PLAYING_STATE_CHANGED", - "android.bluetooth.a2dp-sink.profile.action.AUDIO_CONFIG_CHANGED", - "android.bluetooth.avrcp-controller.profile.action.BROWSE_CONNECTION_STATE_CHANGED", - "android.bluetooth.avrcp-controller.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST", - "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT", - "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.input.profile.action.IDLE_TIME_CHANGED", - "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED", - "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS", - "android.bluetooth.hiddevice.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.map.profile.action.CONNECTION_STATE_CHANGED", - "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT", - "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY", - "android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED", - "android.bluetooth.action.TETHERING_STATE_CHANGED", - "com.android.internal.action.EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS", - "android.net.ConnectivityService.action.PKT_CNT_SAMPLE_INTERVAL_ELAPSED", - "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION", - "com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM" - ) - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt deleted file mode 100644 index 227cdcdc2fec..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.aidl - -import com.android.tools.lint.client.api.UElementHandler -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.SourceCodeScanner -import org.jetbrains.uast.UBlockExpression -import org.jetbrains.uast.UElement -import org.jetbrains.uast.UMethod - -/** - * Abstract class for detectors that look for methods implementing - * generated AIDL interface stubs - */ -abstract class AidlImplementationDetector : Detector(), SourceCodeScanner { - override fun getApplicableUastTypes(): List> = - listOf(UMethod::class.java) - - override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) - - private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { - override fun visitMethod(node: UMethod) { - val interfaceName = getContainingAidlInterface(node) - .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return - val body = (node.uastBody as? UBlockExpression) ?: return - visitAidlMethod(context, node, interfaceName, body) - } - } - - abstract fun visitAidlMethod( - context: JavaContext, - node: UMethod, - interfaceName: String, - body: UBlockExpression, - ) -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt deleted file mode 100644 index 8ee3763e5079..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/Constants.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.aidl - -const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission" -const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission" -const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced" - -val AIDL_PERMISSION_ANNOTATIONS = listOf( - ANNOTATION_ENFORCE_PERMISSION, - ANNOTATION_REQUIRES_NO_PERMISSION, - ANNOTATION_PERMISSION_MANUALLY_ENFORCED -) - -const val IINTERFACE_INTERFACE = "android.os.IInterface" - -/** - * If a non java (e.g. c++) backend is enabled, the @EnforcePermission - * annotation cannot be used. At time of writing, the mechanism - * is not implemented for non java backends. - * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled) - * rather than hard coding this list? - */ -val EXCLUDED_CPP_INTERFACES = listOf( - "AdbTransportType", - "FingerprintAndPairDevice", - "IAdbCallback", - "IAdbManager", - "PairDevice", - "IStatsBootstrapAtomService", - "StatsBootstrapAtom", - "StatsBootstrapAtomValue", - "FixedSizeArrayExample", - "PlaybackTrackMetadata", - "RecordTrackMetadata", - "SinkMetadata", - "SourceMetadata", - "IUpdateEngineStable", - "IUpdateEngineStableCallback", - "AudioCapabilities", - "ConfidenceLevel", - "ModelParameter", - "ModelParameterRange", - "Phrase", - "PhraseRecognitionEvent", - "PhraseRecognitionExtra", - "PhraseSoundModel", - "Properties", - "RecognitionConfig", - "RecognitionEvent", - "RecognitionMode", - "RecognitionStatus", - "SoundModel", - "SoundModelType", - "Status", - "IThermalService", - "IPowerManager", - "ITunerResourceManager" -) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt deleted file mode 100644 index bba819cd9096..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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.aidl - -import com.android.tools.lint.client.api.UElementHandler -import com.android.tools.lint.detector.api.AnnotationInfo -import com.android.tools.lint.detector.api.AnnotationOrigin -import com.android.tools.lint.detector.api.AnnotationUsageInfo -import com.android.tools.lint.detector.api.AnnotationUsageType -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.ConstantEvaluator -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.PsiAnnotation -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UAnnotation -import org.jetbrains.uast.UClass -import org.jetbrains.uast.UElement -import org.jetbrains.uast.UMethod - -/** - * Lint Detector that ensures that any method overriding a method annotated - * with @EnforcePermission is also annotated with the exact same annotation. - * The intent is to surface the effective permission checks to the service - * implementations. - * - * This is done with 2 mechanisms: - * 1. Visit any annotation usage, to ensure that any derived class will have - * the correct annotation on each methods. This is for the top to bottom - * propagation. - * 2. Visit any annotation, to ensure that if a method is annotated, it has - * its ancestor also annotated. This is to avoid having an annotation on a - * Java method without the corresponding annotation on the AIDL interface. - */ -class EnforcePermissionDetector : Detector(), SourceCodeScanner { - - val BINDER_CLASS = "android.os.Binder" - val JAVA_OBJECT = "java.lang.Object" - - override fun applicableAnnotations(): List { - return listOf(ANNOTATION_ENFORCE_PERMISSION) - } - - override fun getApplicableUastTypes(): List> { - return listOf(UAnnotation::class.java) - } - - private fun areAnnotationsEquivalent( - context: JavaContext, - anno1: PsiAnnotation, - anno2: PsiAnnotation - ): Boolean { - if (anno1.qualifiedName != anno2.qualifiedName) { - return false - } - val attr1 = anno1.parameterList.attributes - val attr2 = anno2.parameterList.attributes - if (attr1.size != attr2.size) { - return false - } - for (i in attr1.indices) { - if (attr1[i].name != attr2[i].name) { - return false - } - val value1 = attr1[i].value - val value2 = attr2[i].value - if (value1 == null && value2 == null) { - continue - } - if (value1 == null || value2 == null) { - return false - } - val v1 = ConstantEvaluator.evaluate(context, value1) - val v2 = ConstantEvaluator.evaluate(context, value2) - if (v1 != v2) { - return false - } - } - return true - } - - private fun compareMethods( - context: JavaContext, - element: UElement, - overridingMethod: PsiMethod, - overriddenMethod: PsiMethod, - checkEquivalence: Boolean = true - ) { - val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) - val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) - val location = context.getLocation(element) - val overridingClass = overridingMethod.parent as PsiClass - val overriddenClass = overriddenMethod.parent as PsiClass - val overridingName = "${overridingClass.name}.${overridingMethod.name}" - val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" - if (overridingAnnotation == null) { - val msg = "The method $overridingName overrides the method $overriddenName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $overridingName" - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (overriddenAnnotation == null) { - val msg = "The method $overridingName overrides the method $overriddenName which " + - "is not annotated with @EnforcePermission. The same annotation must be " + - "used on $overriddenName. Did you forget to annotate the AIDL definition?" - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (checkEquivalence && !areAnnotationsEquivalent( - context, overridingAnnotation, overriddenAnnotation)) { - val msg = "The method $overridingName is annotated with " + - "${overridingAnnotation.text} which differs from the overridden " + - "method $overriddenName: ${overriddenAnnotation.text}. The same " + - "annotation must be used for both methods." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } - - private fun compareClasses( - context: JavaContext, - element: UElement, - newClass: PsiClass, - extendedClass: PsiClass, - checkEquivalence: Boolean = true - ) { - val newAnnotation = newClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) - val extendedAnnotation = extendedClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) - - val location = context.getLocation(element) - val newClassName = newClass.qualifiedName - val extendedClassName = extendedClass.qualifiedName - if (newAnnotation == null) { - val msg = "The class $newClassName extends the class $extendedClassName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $newClassName." - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (extendedAnnotation == null) { - val msg = "The class $newClassName extends the class $extendedClassName which " + - "is not annotated with @EnforcePermission. The same annotation must be used " + - "on $extendedClassName. Did you forget to annotate the AIDL definition?" - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (checkEquivalence && !areAnnotationsEquivalent( - context, newAnnotation, extendedAnnotation)) { - val msg = "The class $newClassName is annotated with ${newAnnotation.text} " + - "which differs from the parent class $extendedClassName: " + - "${extendedAnnotation.text}. The same annotation must be used for " + - "both classes." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } - - override fun visitAnnotationUsage( - context: JavaContext, - element: UElement, - annotationInfo: AnnotationInfo, - usageInfo: AnnotationUsageInfo - ) { - if (usageInfo.type == AnnotationUsageType.EXTENDS) { - val newClass = element.sourcePsi?.parent?.parent as PsiClass - val extendedClass: PsiClass = usageInfo.referenced as PsiClass - compareClasses(context, element, newClass, extendedClass) - } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && - annotationInfo.origin == AnnotationOrigin.METHOD) { - val overridingMethod = element.sourcePsi as PsiMethod - val overriddenMethod = usageInfo.referenced as PsiMethod - compareMethods(context, element, overridingMethod, overriddenMethod) - } - } - - override fun createUastHandler(context: JavaContext): UElementHandler { - return object : UElementHandler() { - override fun visitAnnotation(node: UAnnotation) { - if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) { - return - } - val method = node.uastParent as? UMethod - val klass = node.uastParent as? UClass - if (klass != null) { - val newClass = klass as PsiClass - val extendedClass = newClass.getSuperClass() - if (extendedClass != null && extendedClass.qualifiedName != JAVA_OBJECT) { - // The equivalence check can be skipped, if both classes are - // annotated, it will be verified by visitAnnotationUsage. - compareClasses(context, klass, newClass, - extendedClass, checkEquivalence = false) - } - } else if (method != null) { - val overridingMethod = method as PsiMethod - val parents = overridingMethod.findSuperMethods() - for (overriddenMethod in parents) { - // The equivalence check can be skipped, if both methods are - // annotated, it will be verified by visitAnnotationUsage. - compareMethods(context, method, overridingMethod, - overriddenMethod, checkEquivalence = false) - } - } - } - } - } - - companion object { - val EXPLANATION = """ - The @EnforcePermission annotation is used to indicate that the underlying binder code - has already verified the caller's permissions before calling the appropriate method. The - verification code is usually generated by the AIDL compiler, which also takes care of - annotating the generated Java code. - - In order to surface that information to platform developers, the same annotation must be - used on the implementation class or methods. - """ - - val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MissingEnforcePermissionAnnotation", - briefDescription = "Missing @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - - val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MismatchingEnforcePermissionAnnotation", - briefDescription = "Incorrect @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt deleted file mode 100644 index d120e1d41c99..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.aidl - -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Location -import com.android.tools.lint.detector.api.getUMethod -import org.jetbrains.kotlin.psi.psiUtil.parameterIndex -import org.jetbrains.uast.UCallExpression -import org.jetbrains.uast.evaluateString -import org.jetbrains.uast.visitor.AbstractUastVisitor - -/** - * Helper class that facilitates the creation of lint auto fixes - * - * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks - * that should be migrated to @EnforcePermission(allOf={...}) - * - * TODO: handle anyOf style annotations - */ -data class EnforcePermissionFix( - val locations: List, - val permissionNames: List -) { - val annotation: String - get() { - val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } - val annotationParameter = - if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions - return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" - } - - companion object { - /** - * conditionally constructs EnforcePermissionFix from a UCallExpression - * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null - */ - fun fromCallExpression( - context: JavaContext, - callExpression: UCallExpression - ): EnforcePermissionFix? = - if (isPermissionMethodCall(callExpression)) { - EnforcePermissionFix( - listOf(getPermissionCheckLocation(context, callExpression)), - getPermissionCheckValues(callExpression) - ) - } else null - - - fun compose(individuals: List): EnforcePermissionFix = - EnforcePermissionFix( - individuals.flatMap { it.locations }, - individuals.flatMap { it.permissionNames } - ) - - /** - * Given a permission check, get its proper location - * so that a lint fix can remove the entire expression - */ - private fun getPermissionCheckLocation( - context: JavaContext, - callExpression: UCallExpression - ): - Location { - val javaPsi = callExpression.javaPsi!! - return Location.create( - context.file, - javaPsi.containingFile?.text, - javaPsi.textRange.startOffset, - // unfortunately the element doesn't include the ending semicolon - javaPsi.textRange.endOffset + 1 - ) - } - - /** - * Given a @PermissionMethod, find arguments annotated with @PermissionName - * and pull out the permission value(s) being used. Also evaluates nested calls - * to @PermissionMethod(s) in the given method's body. - */ - private fun getPermissionCheckValues( - callExpression: UCallExpression - ): List { - if (!isPermissionMethodCall(callExpression)) return emptyList() - - val result = mutableSetOf() // protect against duplicate permission values - val visitedCalls = mutableSetOf() // don't visit the same call twice - val bfsQueue = ArrayDeque(listOf(callExpression)) - - // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available - // source code for @PermissionName(s). - while (bfsQueue.isNotEmpty()) { - val current = bfsQueue.removeFirst() - visitedCalls.add(current) - result.addAll(findPermissions(current)) - - current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() { - override fun visitCallExpression(node: UCallExpression): Boolean { - if (isPermissionMethodCall(node) && node !in visitedCalls) { - bfsQueue.add(node) - } - return false - } - }) - } - - return result.toList() - } - - private fun findPermissions( - callExpression: UCallExpression, - ): List { - val indices = callExpression.resolve()?.getUMethod() - ?.uastParameters - ?.filter(::hasPermissionNameAnnotation) - ?.mapNotNull { it.sourcePsi?.parameterIndex() } - ?: emptyList() - - return indices.mapNotNull { - callExpression.getArgumentForParameter(it)?.evaluateString() - } - } - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt deleted file mode 100644 index 3c2ea1db0ad6..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.aidl - -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.intellij.psi.PsiElement -import org.jetbrains.uast.UBlockExpression -import org.jetbrains.uast.UDeclarationsExpression -import org.jetbrains.uast.UExpression -import org.jetbrains.uast.UElement -import org.jetbrains.uast.UMethod - -class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { - override fun getApplicableUastTypes(): List> = - listOf(UMethod::class.java) - - override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) - - private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { - override fun visitMethod(node: UMethod) { - if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return - - val targetExpression = "super.${node.name}$HELPER_SUFFIX()" - - val body = node.uastBody as? UBlockExpression - if (body == null) { - context.report( - ISSUE_ENFORCE_PERMISSION_HELPER, - context.getLocation(node), - "Method must start with $targetExpression", - ) - return - } - - val firstExpression = body.expressions.firstOrNull() - if (firstExpression == null) { - context.report( - ISSUE_ENFORCE_PERMISSION_HELPER, - context.getLocation(node), - "Method must start with $targetExpression", - ) - return - } - - val firstExpressionSource = firstExpression.asSourceString() - .filterNot(Char::isWhitespace) - - if (firstExpressionSource != targetExpression) { - val locationTarget = getLocationTarget(firstExpression) - val expressionLocation = context.getLocation(locationTarget) - val indent = " ".repeat(expressionLocation.start?.column ?: 0) - - val fix = fix() - .replace() - .range(expressionLocation) - .beginning() - .with("$targetExpression;\n\n$indent") - .reformat(true) - .autoFix() - .build() - - context.report( - ISSUE_ENFORCE_PERMISSION_HELPER, - context.getLocation(node), - "Method must start with $targetExpression", - fix - ) - } - } - } - - companion object { - private const val HELPER_SUFFIX = "_enforcePermission" - - private const val EXPLANATION = """ - When @EnforcePermission is applied, the AIDL compiler generates a Stub method to do the - permission check called yourMethodName$HELPER_SUFFIX. - - You must call this method as the first expression in your implementation. - """ - - val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( - id = "MissingEnforcePermissionHelper", - briefDescription = """Missing permission-enforcing method call in AIDL method - |annotated with @EnforcePermission""".trimMargin(), - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionHelperDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - - /** - * handles an edge case with UDeclarationsExpression, where sourcePsi is null, - * resulting in an incorrect Location if used directly - */ - private fun getLocationTarget(firstExpression: UExpression): PsiElement? { - if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi - if (firstExpression is UDeclarationsExpression) { - return firstExpression.declarations.firstOrNull()?.sourcePsi - } - return null - } - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt deleted file mode 100644 index edbdd8d2adf3..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.aidl - -import com.android.tools.lint.detector.api.getUMethod -import com.google.android.lint.ANNOTATION_PERMISSION_METHOD -import com.google.android.lint.ANNOTATION_PERMISSION_NAME -import com.google.android.lint.CLASS_STUB -import com.intellij.psi.PsiAnonymousClass -import org.jetbrains.uast.UCallExpression -import org.jetbrains.uast.UMethod -import org.jetbrains.uast.UParameter - -/** - * Given a UMethod, determine if this method is - * an entrypoint to an interface generated by AIDL, - * returning the interface name if so - */ -fun getContainingAidlInterface(node: UMethod): String? { - if (!isInClassCalledStub(node)) return null - for (superMethod in node.findSuperMethods()) { - for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements - ?: continue) { - if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { - return superMethod.containingClass?.name - } - } - } - return null -} - -private fun isInClassCalledStub(node: UMethod): Boolean { - (node.containingClass as? PsiAnonymousClass)?.let { - return it.baseClassReference.referenceName == CLASS_STUB - } - return node.containingClass?.extendsList?.referenceElements?.any { - it.referenceName == CLASS_STUB - } ?: false -} - -fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { - val method = callExpression.resolve()?.getUMethod() ?: return false - return hasPermissionMethodAnnotation(method) -} - -fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations - .any { - it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) - } - -fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { - it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt deleted file mode 100644 index 4c0cbe7b3adf..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.aidl - -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.Implementation -import com.android.tools.lint.detector.api.Issue -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Scope -import com.android.tools.lint.detector.api.Severity -import org.jetbrains.uast.UBlockExpression -import org.jetbrains.uast.UCallExpression -import org.jetbrains.uast.UElement -import org.jetbrains.uast.UIfExpression -import org.jetbrains.uast.UMethod -import org.jetbrains.uast.UQualifiedReferenceExpression - -/** - * Looks for methods implementing generated AIDL interface stubs - * that can have simple permission checks migrated to - * @EnforcePermission annotations - * - * TODO: b/242564870 (enable parse and autoFix of .aidl files) - */ -@Suppress("UnstableApiUsage") -class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { - override fun visitAidlMethod( - context: JavaContext, - node: UMethod, - interfaceName: String, - body: UBlockExpression - ) { - val fix = accumulateSimplePermissionCheckFixes(body, context) ?: return - - val javaRemoveFixes = fix.locations.map { - fix() - .replace() - .reformat(true) - .range(it) - .with("") - .autoFix() - .build() - } - - val javaAnnotateFix = fix() - .annotate(fix.annotation) - .range(context.getLocation(node)) - .autoFix() - .build() - - context.report( - ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, - fix.locations.last(), - "$interfaceName permission check can be converted to @EnforcePermission annotation", - fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix) - ) - } - - /** - * Walk the expressions in the method, looking for simple permission checks. - * - * If a single permission check is found at the beginning of the method, - * this should be migrated to @EnforcePermission(value). - * - * If multiple consecutive permission checks are found, - * these should be migrated to @EnforcePermission(allOf={value1, value2, ...}) - * - * As soon as something other than a permission check is encountered, stop looking, - * as some other business logic is happening that prevents an automated fix. - */ - private fun accumulateSimplePermissionCheckFixes( - methodBody: UBlockExpression, - context: JavaContext - ): - EnforcePermissionFix? { - val singleFixes = mutableListOf() - for (expression in methodBody.expressions) { - singleFixes.add(getPermissionCheckFix(expression, context) ?: break) - } - return when (singleFixes.size) { - 0 -> null - 1 -> singleFixes[0] - else -> EnforcePermissionFix.compose(singleFixes) - } - } - - /** - * If an expression boils down to a permission check, return - * the helper for creating a lint auto fix to @EnforcePermission - */ - private fun getPermissionCheckFix(startingExpression: UElement?, context: JavaContext): - EnforcePermissionFix? { - return when (startingExpression) { - is UQualifiedReferenceExpression -> getPermissionCheckFix( - startingExpression.selector, context - ) - - is UIfExpression -> getPermissionCheckFix(startingExpression.condition, context) - - is UCallExpression -> return EnforcePermissionFix - .fromCallExpression(context, startingExpression) - - else -> null - } - } - - companion object { - - private val EXPLANATION = """ - Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission - annotation to declare the permissions to be enforced. The verification code is then - generated by the AIDL compiler, which also takes care of annotating the generated java - code. - - This reduces the risk of bugs around these permission checks (that often become vulnerabilities). - It also enables easier auditing and review. - - Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto) - """.trimIndent() - - @JvmField - val ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION = Issue.create( - id = "SimpleManualPermissionEnforcement", - briefDescription = "Manual permission check can be @EnforcePermission annotation", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 5, - severity = Severity.WARNING, - implementation = Implementation( - SimpleManualPermissionEnforcementDetector::class.java, - Scope.JAVA_FILE_SCOPE - ), - enabledByDefault = false, // TODO: enable once b/241171714 is resolved - ) - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt b/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt deleted file mode 100644 index 3939b6109eaa..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/model/Method.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.model - -/** - * Data class to represent a Method - */ -data class Method(val clazz: String, val name: String) { - override fun toString(): String { - return "$clazz#$name" - } -} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt deleted file mode 100644 index 06c098df385d..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt +++ /dev/null @@ -1,229 +0,0 @@ -/* - * 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 = 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 = - 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 = 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>) (Class) Foo.class. - * This is needed for when the arguments of the conflict (eg. when there is List> and - * class type is Class) (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 = 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 = 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 = 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 = 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/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt deleted file mode 100644 index 0826e8e74431..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/Method.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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, - val clazz: String, - val name: String, - val parameters: List -) { - constructor( - clazz: String, - name: String, - parameters: List - ) : 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/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt deleted file mode 100644 index f92826316be4..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 = - 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 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", "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/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt deleted file mode 100644 index d90f3e31baf9..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt +++ /dev/null @@ -1,867 +0,0 @@ -/* - * 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 = 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/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt deleted file mode 100644 index e72f38416310..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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 = 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/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt deleted file mode 100644 index a70644ab8532..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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 = 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/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt deleted file mode 100644 index b76342a81972..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt +++ /dev/null @@ -1,560 +0,0 @@ -/* - * 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 RegisterReceiverFlagDetectorTest : LintDetectorTest() { - - override fun getDetector(): Detector = RegisterReceiverFlagDetector() - - override fun getIssues(): List = listOf( - RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG - ) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - fun testProtectedBroadcast() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testProtectedBroadcastCreate() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = - IntentFilter.create(Intent.ACTION_BATTERY_CHANGED, "foo/bar"); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testMultipleProtectedBroadcasts() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - filter.addAction(Intent.ACTION_BATTERY_LOW); - filter.addAction(Intent.ACTION_BATTERY_OKAY); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testSubsequentFilterModification() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - filter.addAction(Intent.ACTION_BATTERY_LOW); - filter.addAction(Intent.ACTION_BATTERY_OKAY); - context.registerReceiver(receiver, filter); - filter.addAction("querty"); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.java:13: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testNullReceiver() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context) { - IntentFilter filter = new IntentFilter("qwerty"); - context.registerReceiver(null, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testExportedFlagPresent() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter("qwerty"); - context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testNotExportedFlagPresent() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter("qwerty"); - context.registerReceiver(receiver, filter, - Context.RECEIVER_NOT_EXPORTED); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testFlagArgumentAbsent() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter("qwerty"); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testExportedFlagsAbsent() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter("qwerty"); - context.registerReceiver(receiver, filter, 0); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter, 0); - ~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testExportedFlagVariable() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter("qwerty"); - var flags = Context.RECEIVER_EXPORTED; - context.registerReceiver(receiver, filter, flags); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testUnknownFilter() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver, - IntentFilter filter) { - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testFilterEscapes() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - updateFilter(filter); - context.registerReceiver(receiver, filter); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.java:10: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testInlineFilter() { - lint().files( - java( - """ - package test.pkg; - import android.content.BroadcastReceiver; - import android.content.Context; - import android.content.Intent; - import android.content.IntentFilter; - public class TestClass1 { - public void testMethod(Context context, BroadcastReceiver receiver) { - context.registerReceiver(receiver, - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testInlineFilterApply() { - lint().files( - kotlin( - """ - package test.pkg - import android.content.BroadcastReceiver - import android.content.Context - import android.content.Intent - import android.content.IntentFilter - class TestClass1 { - fun test(context: Context, receiver: BroadcastReceiver) { - context.registerReceiver(receiver, - IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { - addAction("qwerty") - }) - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.kt:8: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, - ^ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testFilterVariableApply() { - lint().files( - kotlin( - """ - package test.pkg - import android.content.BroadcastReceiver - import android.content.Context - import android.content.Intent - import android.content.IntentFilter - class TestClass1 { - fun test(context: Context, receiver: BroadcastReceiver) { - val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { - addAction("qwerty") - } - context.registerReceiver(receiver, filter) - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.kt:11: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testFilterVariableApply2() { - lint().files( - kotlin( - """ - package test.pkg - import android.content.BroadcastReceiver - import android.content.Context - import android.content.Intent - import android.content.IntentFilter - class TestClass1 { - fun test(context: Context, receiver: BroadcastReceiver) { - val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { - addAction(Intent.ACTION_BATTERY_OKAY) - } - context.registerReceiver(receiver, filter.apply { - addAction("qwerty") - }) - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.kt:11: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter.apply { - ^ - 0 errors, 1 warnings - """.trimIndent()) - } - - fun testFilterComplexChain() { - lint().files( - kotlin( - """ - package test.pkg - import android.content.BroadcastReceiver - import android.content.Context - import android.content.Intent - import android.content.IntentFilter - class TestClass1 { - fun test(context: Context, receiver: BroadcastReceiver) { - val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { - addAction(Intent.ACTION_BATTERY_OKAY) - } - val filter2 = filter - val filter3 = filter2.apply { - addAction(Intent.ACTION_BATTERY_LOW) - } - context.registerReceiver(receiver, filter3) - val filter4 = filter3.apply { - addAction("qwerty") - } - context.registerReceiver(receiver, filter4) - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect(""" - src/test/pkg/TestClass1.kt:19: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] - context.registerReceiver(receiver, filter4) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """.trimIndent()) - } - - private val broadcastReceiverStub: TestFile = java( - """ - package android.content; - public class BroadcastReceiver { - // Stub - } - """ - ).indented() - - private val contextStub: TestFile = java( - """ - package android.content; - public class Context { - public static final int RECEIVER_EXPORTED = 0x2; - public static final int RECEIVER_NOT_EXPORTED = 0x4; - @Nullable - public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver, - IntentFilter filter, - @RegisterReceiverFlags int flags); - } - """ - ).indented() - - private val intentStub: TestFile = java( - """ - package android.content; - public class Intent { - public static final String ACTION_BATTERY_CHANGED = - "android.intent.action.BATTERY_CHANGED"; - public static final String ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW"; - public static final String ACTION_BATTERY_OKAY = - "android.intent.action.BATTERY_OKAY"; - } - """ - ).indented() - - private val intentFilterStub: TestFile = java( - """ - package android.content; - public class IntentFilter { - public IntentFilter() { - // Stub - } - public IntentFilter(String action) { - // Stub - } - public IntentFilter(String action, String dataType) { - // Stub - } - public static IntentFilter create(String action, String dataType) { - return null; - } - public final void addAction(String action) { - // Stub - } - } - """ - ).indented() - - private val stubs = arrayOf(broadcastReceiverStub, contextStub, intentStub, intentFilterStub) -} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt deleted file mode 100644 index 3c1d1e8e8a59..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * 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.aidl - -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 EnforcePermissionDetectorTest : LintDetectorTest() { - override fun getDetector(): Detector = EnforcePermissionDetector() - - override fun getIssues(): List = listOf( - EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION - ) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - fun testDoesNotDetectIssuesCorrectAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public class TestClass1 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - import android.annotation.EnforcePermission; - public class TestClass2 extends IFooMethod.Stub { - @Override - @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDetectIssuesMismatchingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public class TestClass3 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the parent class IFoo.Stub: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \ -same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation] -public class TestClass3 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMismatchingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass4 extends IFooMethod.Stub { - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the overridden method Stub.testMethod: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \ -annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - public class TestClass5 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \ -the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \ -used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation] -public class TestClass5 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass6 extends IFooMethod.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \ -overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \ -annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesExtraAnnotationMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass7 extends IBar.Stub { - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod \ -overrides the method Stub.testMethod which is not annotated with @EnforcePermission. The same \ -annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? \ -[MissingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesExtraAnnotationInterface() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public class TestClass8 extends IBar.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass8.java:2: Error: The class test.pkg.TestClass8 \ -extends the class IBar.Stub which is not annotated with @EnforcePermission. The same annotation \ -must be used on IBar.Stub. Did you forget to annotate the AIDL definition? \ -[MissingEnforcePermissionAnnotation] -@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) -^ -1 errors, 0 warnings""".addLineContinuation()) - } - - /* Stubs */ - - // A service with permission annotation on the class. - private val interfaceIFooStub: TestFile = java( - """ - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public interface IFoo { - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public static abstract class Stub extends android.os.Binder implements IFoo { - @Override - public void testMethod() {} - } - public void testMethod(); - } - """ - ).indented() - - // A service with permission annotation on the method. - private val interfaceIFooMethodStub: TestFile = java( - """ - public interface IFooMethod { - public static abstract class Stub extends android.os.Binder implements IFooMethod { - @Override - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod(); - } - """ - ).indented() - - // A service without any permission annotation. - private val interfaceIBarStub: TestFile = java( - """ - public interface IBar { - public static abstract class Stub extends android.os.Binder implements IBar { - @Override - public void testMethod() {} - } - public void testMethod(); - } - """ - ).indented() - - private val manifestPermissionStub: TestFile = java( - """ - package android.Manifest; - class permission { - public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; - public static final String INTERNET = "android.permission.INTERNET"; - } - """ - ).indented() - - private val enforcePermissionAnnotationStub: TestFile = java( - """ - package android.annotation; - public @interface EnforcePermission {} - """ - ).indented() - - private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, interfaceIBarStub, - manifestPermissionStub, enforcePermissionAnnotationStub) - - // 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/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt deleted file mode 100644 index 31e484628a04..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* -* 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.aidl - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestLintTask - -class EnforcePermissionHelperDetectorTest : LintDetectorTest() { - override fun getDetector() = EnforcePermissionHelperDetector() - override fun getIssues() = listOf( - EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk() - - fun testFirstExpressionIsFunctionCall() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo extends ITest.Stub { - private Context mContext; - @Override - @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") - public void test() throws android.os.RemoteException { - Binder.getCallingUid(); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] - @Override - ^ - 1 errors, 0 warnings - """ - ) - .expectFixDiffs( - """ - Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: - @@ -8 +8 - + super.test_enforcePermission(); - + - """ - ) - } - - fun testFirstExpressionIsVariableDeclaration() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo extends ITest.Stub { - private Context mContext; - @Override - @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") - public void test() throws android.os.RemoteException { - String foo = "bar"; - Binder.getCallingUid(); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] - @Override - ^ - 1 errors, 0 warnings - """ - ) - .expectFixDiffs( - """ - Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: - @@ -8 +8 - + super.test_enforcePermission(); - + - """ - ) - } - - fun testMethodIsEmpty() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo extends ITest.Stub { - private Context mContext; - @Override - @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") - public void test() throws android.os.RemoteException {} - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] - @Override - ^ - 1 errors, 0 warnings - """ - ) - } - - fun testOkay() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo extends ITest.Stub { - private Context mContext; - @Override - @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") - public void test() throws android.os.RemoteException { - super.test_enforcePermission(); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - companion object { - val stubs = arrayOf(aidlStub, contextStub, binderStub) - } -} - - - diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt deleted file mode 100644 index 150fc264506f..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt +++ /dev/null @@ -1,371 +0,0 @@ -/* - * 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.aidl - -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 SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { - override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector() - override fun getIssues(): List = listOf( - SimpleManualPermissionEnforcementDetector - .ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION - ) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk() - - fun testClass() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo extends ITest.Stub { - private Context mContext; - @Override - public void test() throws android.os.RemoteException { - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 7: Annotate with @EnforcePermission: - @@ -5 +5 - + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") - @@ -7 +8 - - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - """ - ) - } - - fun testAnonClass() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo { - private Context mContext; - private ITest itest = new ITest.Stub() { - @Override - public void test() throws android.os.RemoteException { - mContext.enforceCallingOrSelfPermission( - "android.permission.READ_CONTACTS", "foo"); - } - }; - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - mContext.enforceCallingOrSelfPermission( - ^ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 8: Annotate with @EnforcePermission: - @@ -6 +6 - + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") - @@ -8 +9 - - mContext.enforceCallingOrSelfPermission( - - "android.permission.READ_CONTACTS", "foo"); - """ - ) - } - - fun testConstantEvaluation() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - - public class Foo extends ITest.Stub { - private Context mContext; - @Override - public void test() throws android.os.RemoteException { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); - } - } - """ - ).indented(), - *stubs, - manifestStub - ) - .run() - .expect( - """ - src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 7: Annotate with @EnforcePermission: - @@ -6 +6 - + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") - @@ -8 +9 - - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); - """ - ) - } - - fun testAllOf() { - lint().files( - java( - """ - import android.content.Context; - import android.test.ITest; - public class Foo { - private Context mContext; - private ITest itest = new ITest.Stub() { - @Override - public void test() throws android.os.RemoteException { - mContext.enforceCallingOrSelfPermission( - "android.permission.READ_CONTACTS", "foo"); - mContext.enforceCallingOrSelfPermission( - "android.permission.WRITE_CONTACTS", "foo"); - } - }; - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - mContext.enforceCallingOrSelfPermission( - ^ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 10: Annotate with @EnforcePermission: - @@ -6 +6 - + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) - @@ -8 +9 - - mContext.enforceCallingOrSelfPermission( - - "android.permission.READ_CONTACTS", "foo"); - - mContext.enforceCallingOrSelfPermission( - - "android.permission.WRITE_CONTACTS", "foo"); - """ - ) - } - - fun testPrecedingExpressions() { - lint().files( - java( - """ - import android.os.Binder; - import android.test.ITest; - public class Foo extends ITest.Stub { - private mContext Context; - @Override - public void test() throws android.os.RemoteException { - long uid = Binder.getCallingUid(); - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testPermissionHelper() { - lint().skipTestModes(TestMode.PARENTHESIZED).files( - java( - """ - import android.content.Context; - import android.test.ITest; - - public class Foo extends ITest.Stub { - private Context mContext; - - @android.content.pm.PermissionMethod - private void helper() { - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - } - - @Override - public void test() throws android.os.RemoteException { - helper(); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - helper(); - ~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 14: Annotate with @EnforcePermission: - @@ -12 +12 - + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") - @@ -14 +15 - - helper(); - """ - ) - } - - fun testPermissionHelperAllOf() { - lint().skipTestModes(TestMode.PARENTHESIZED).files( - java( - """ - import android.content.Context; - import android.test.ITest; - - public class Foo extends ITest.Stub { - private Context mContext; - - @android.content.pm.PermissionMethod - private void helper() { - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo"); - } - - @Override - public void test() throws android.os.RemoteException { - helper(); - mContext.enforceCallingOrSelfPermission("FOO", "foo"); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - mContext.enforceCallingOrSelfPermission("FOO", "foo"); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 16: Annotate with @EnforcePermission: - @@ -13 +13 - + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"}) - @@ -15 +16 - - helper(); - - mContext.enforceCallingOrSelfPermission("FOO", "foo"); - """ - ) - } - - - fun testPermissionHelperNested() { - lint().skipTestModes(TestMode.PARENTHESIZED).files( - java( - """ - import android.content.Context; - import android.test.ITest; - - public class Foo extends ITest.Stub { - private Context mContext; - - @android.content.pm.PermissionMethod - private void helperHelper() { - helper("android.permission.WRITE_CONTACTS"); - } - - @android.content.pm.PermissionMethod - private void helper(@android.content.pm.PermissionName String extraPermission) { - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); - } - - @Override - public void test() throws android.os.RemoteException { - helperHelper(); - } - } - """ - ).indented(), - *stubs - ) - .run() - .expect( - """ - src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] - helperHelper(); - ~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - .expectFixDiffs( - """ - Fix for src/Foo.java line 19: Annotate with @EnforcePermission: - @@ -17 +17 - + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"}) - @@ -19 +20 - - helperHelper(); - """ - ) - } - - - - companion object { - val stubs = arrayOf( - aidlStub, - contextStub, - binderStub, - permissionMethodStub, - permissionNameStub - ) - } -} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt deleted file mode 100644 index bd6b1952847c..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.google.android.lint.aidl - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java -import com.android.tools.lint.checks.infrastructure.TestFile - -val aidlStub: TestFile = java( - """ - package android.test; - public interface ITest extends android.os.IInterface { - public static abstract class Stub extends android.os.Binder implements android.test.ITest {} - public void test() throws android.os.RemoteException; - } - """ -).indented() - -val contextStub: TestFile = java( - """ - package android.content; - public class Context { - @android.content.pm.PermissionMethod - public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} - } - """ -).indented() - -val binderStub: TestFile = java( - """ - package android.os; - public class Binder { - public static int getCallingUid() {} - } - """ -).indented() - -val permissionMethodStub: TestFile = java( -""" - package android.content.pm; - - import static java.lang.annotation.ElementType.METHOD; - import static java.lang.annotation.RetentionPolicy.CLASS; - - import java.lang.annotation.Retention; - import java.lang.annotation.Target; - - @Retention(CLASS) - @Target({METHOD}) - public @interface PermissionMethod {} - """ -).indented() - -val permissionNameStub: TestFile = java( -""" - package android.content.pm; - - import static java.lang.annotation.ElementType.FIELD; - import static java.lang.annotation.ElementType.LOCAL_VARIABLE; - import static java.lang.annotation.ElementType.METHOD; - import static java.lang.annotation.ElementType.PARAMETER; - import static java.lang.annotation.RetentionPolicy.CLASS; - - import java.lang.annotation.Retention; - import java.lang.annotation.Target; - - @Retention(CLASS) - @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) - public @interface PermissionName {} - """ -).indented() - -val manifestStub: TestFile = java( - """ - package android; - - public final class Manifest { - public static final class permission { - public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; - } - } - """.trimIndent() -) \ No newline at end of file diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt deleted file mode 100644 index e686695ca804..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt +++ /dev/null @@ -1,823 +0,0 @@ -/* - * 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 = 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 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 list = new ArrayList(); - 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 list = new ArrayList(); - 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 list = new ArrayList(); - List ans = p.readParcelableList(list, null); - } - } - """ - ).indented(), - *includes - ) - .run() - .expect( - """ - src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \ - API usage [UnsafeParcelApi] - List 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 list = new ArrayList(); - List 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 ans = p.readSparseArray(null); - } - } - """ - ).indented(), - *includes - ) - .run() - .expect( - """ - src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\ - usage [UnsafeParcelApi] - SparseArray 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 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 ans = b.getParcelableArrayList("key"); - } - } - """ - ).indented(), - *includes - ) - .run() - .expect( - """ - src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi] - ArrayList 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 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 ans = b.getSparseParcelableArray("key"); - } - } - """ - ).indented(), - *includes - ) - .run() - .expect( - """ - src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi] - SparseArray 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 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 readParcelable(ClassLoader loader) { return null; } - public Parcelable[] readParcelableArray(ClassLoader loader) { return null; } - public Parcelable.Creator readParcelableCreator(ClassLoader loader) { return null; } - public List readParcelableList(List list, ClassLoader cl) { return null; } - public Serializable readSerializable() { return null; } - public SparseArray readSparseArray(ClassLoader loader) { return null; } - - // Replacements - public T[] readArray(ClassLoader loader, Class clazz) { return null; } - public ArrayList readArrayList(ClassLoader loader, Class clazz) { return null; } - public HashMap readHashMap(ClassLoader loader, Class clazzKey, Class clazzValue) { return null; } - public void readList(List outVal, ClassLoader loader, Class clazz) {} - public void readMap(Map outVal, ClassLoader loader, Class clazzKey, Class clazzValue) {} - public T readParcelable(ClassLoader loader, Class clazz) { return null; } - public T[] readParcelableArray(ClassLoader loader, Class clazz) { return null; } - public Parcelable.Creator readParcelableCreator(ClassLoader loader, Class clazz) { return null; } - public List readParcelableList(List list, ClassLoader cl, Class clazz) { return null; } - public T readSerializable(ClassLoader loader, Class clazz) { return null; } - public SparseArray readSparseArray(ClassLoader loader, Class 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 getParcelable(String key) { return null; } - public ArrayList getParcelableArrayList(String key) { return null; } - public Parcelable[] getParcelableArray(String key) { return null; } - public SparseArray getSparseParcelableArray(String key) { return null; } - - // Replacements - public T getParcelable(String key, Class clazz) { return null; } - public ArrayList getParcelableArrayList(String key, Class clazz) { return null; } - public T[] getParcelableArray(String key, Class clazz) { return null; } - public SparseArray getSparseParcelableArray(String key, Class 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 getParcelableExtra(String name) { return null; } - - // Replacements - public T getParcelableExtra(String name, Class clazz) { return null; } - - } - """ - ).indented(), - java( - """ - package android.util; - public class SparseArray implements Cloneable {} - """ - ).indented(), - ) - - // 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/common/Android.bp b/tools/lint/common/Android.bp new file mode 100644 index 000000000000..898f88b8759c --- /dev/null +++ b/tools/lint/common/Android.bp @@ -0,0 +1,29 @@ +// 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: "AndroidCommonLint", + srcs: ["src/main/java/**/*.kt"], + libs: ["lint_api"], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt new file mode 100644 index 000000000000..3d5d01c9b7a0 --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt @@ -0,0 +1,40 @@ +/* + * 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.google.android.lint.model.Method + +const val CLASS_STUB = "Stub" +const val CLASS_CONTEXT = "android.content.Context" +const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService" +const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal" + +// Enforce permission APIs +val ENFORCE_PERMISSION_METHODS = listOf( + Method(CLASS_CONTEXT, "checkPermission"), + Method(CLASS_CONTEXT, "checkCallingPermission"), + Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), + Method(CLASS_CONTEXT, "enforcePermission"), + Method(CLASS_CONTEXT, "enforceCallingPermission"), + Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), + Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), + Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") +) + +const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod" +const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName" +const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt new file mode 100644 index 000000000000..720f8356f050 --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt @@ -0,0 +1,36 @@ +/* + * 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.detector.api.getUMethod +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter + +fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { + val method = callExpression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(method) +} + +fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations + .any { + it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) + } + +fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { + it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) +} diff --git a/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt new file mode 100644 index 000000000000..3939b6109eaa --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt @@ -0,0 +1,26 @@ +/* + * 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.model + +/** + * Data class to represent a Method + */ +data class Method(val clazz: String, val name: String) { + override fun toString(): String { + return "$clazz#$name" + } +} diff --git a/tools/lint/fix/Android.bp b/tools/lint/fix/Android.bp new file mode 100644 index 000000000000..5f6c6f779f85 --- /dev/null +++ b/tools/lint/fix/Android.bp @@ -0,0 +1,28 @@ +// 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"], +} + +python_binary_host { + name: "lint_fix", + main: "lint_fix.py", + srcs: ["lint_fix.py"], +} diff --git a/tools/lint/framework/Android.bp b/tools/lint/framework/Android.bp new file mode 100644 index 000000000000..7f27e8a57d19 --- /dev/null +++ b/tools/lint/framework/Android.bp @@ -0,0 +1,58 @@ +// 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", + // TODO: remove once b/236558918 is resolved and the below checks actually run globally + "AndroidGlobalLintChecker", + ], + kotlincflags: ["-Xjvm-default=all"], +} + +java_test_host { + name: "AndroidFrameworkLintCheckerTest", + // TODO(b/239881504): Since this test was written, Android + // Lint was updated, and now includes classes that were + // compiled for java 15. The soong build doesn't support + // java 15 yet, so we can't compile against "lint". Disable + // the test until java 15 is supported. + enabled: false, + srcs: ["checks/src/test/java/**/*.kt"], + static_libs: [ + "AndroidFrameworkLintChecker", + "junit", + "lint", + "lint_tests", + ], + test_options: { + unit_test: true, + }, +} 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 000000000000..413e19717d50 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -0,0 +1,62 @@ +/* + * 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.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.EnforcePermissionHelperDetector +import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector +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, + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + SimpleManualPermissionEnforcementDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, + PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, + RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG, + 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 000000000000..0c375c358e61 --- /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 */ + private val tokensMap = mutableMapOf() + + override fun getApplicableUastTypes(): List> = + 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(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 + ) { + 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 000000000000..fe567da7c017 --- /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 = 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 000000000000..48540b1da565 --- /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 + * + * Package Visibility that we introduced in Android 11. + *

+ * 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, + sinkMethodNodes: List + ) { + 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::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 + ): 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 + ): Boolean { + val startEntry = TraceEntry(apiNode, pkgNameParamIndex) + val traceQueue = LinkedList().apply { add(startEntry) } + val allVisits = mutableSetOf().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? { + 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>): 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 000000000000..e12ec3d4a77c --- /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> = + 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/RegisterReceiverFlagDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt new file mode 100644 index 000000000000..c3e0428316c3 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/RegisterReceiverFlagDetector.kt @@ -0,0 +1,927 @@ +/* + * 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.DataFlowAnalyzer +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.ConstantEvaluator +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.UastLintUtils.Companion.findLastAssignment +import com.android.tools.lint.detector.api.getMethodName +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiVariable +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UParenthesizedExpression +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.getContainingUMethod +import org.jetbrains.uast.isNullLiteral +import org.jetbrains.uast.skipParenthesizedExprDown +import org.jetbrains.uast.tryResolve + +/** + * Detector that identifies `registerReceiver()` calls which are missing the `RECEIVER_EXPORTED` or + * `RECEIVER_NOT_EXPORTED` flags on T+. + * + * TODO: Add API level conditions to better support non-platform code. + * 1. Check if registerReceiver() call is reachable on T+. + * 2. Check if targetSdkVersion is T+. + * + * eg: isWithinVersionCheckConditional(context, node, 31, false) + * eg: isPrecededByVersionCheckExit(context, node, 31) ? + */ +@Suppress("UnstableApiUsage") +class RegisterReceiverFlagDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List = listOf( + "registerReceiver", + "registerReceiverAsUser", + "registerReceiverForAllUsers" + ) + + private fun checkIsProtectedReceiverAndReturnUnprotectedActions( + filterArg: UExpression, + node: UCallExpression, + evaluator: ConstantEvaluator + ): Pair> { // isProtected, unprotectedActions + val actions = mutableSetOf() + val construction = findIntentFilterConstruction(filterArg, node) + + if (construction == null) return Pair(false, listOf()) + val constructorActionArg = construction.getArgumentForParameter(0) + (constructorActionArg?.let(evaluator::evaluate) as? String)?.let(actions::add) + + val actionCollectorVisitor = + ActionCollectorVisitor(setOf(construction), node, evaluator) + + val parent = node.getContainingUMethod() + parent?.accept(actionCollectorVisitor) + actions.addAll(actionCollectorVisitor.actions) + + // If we failed to evaluate any actions, there will be a null action in the set. + val isProtected = + actions.all(PROTECTED_BROADCASTS::contains) && + !actionCollectorVisitor.intentFilterEscapesScope + val unprotectedActionsList = actions.filterNot(PROTECTED_BROADCASTS::contains) + return Pair(isProtected, unprotectedActionsList) + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!context.evaluator.isMemberInSubClassOf(method, "android.content.Context")) return + + // The parameter positions vary across the various registerReceiver*() methods, so rather + // than hardcode them we simply look them up based on the parameter name and type. + val receiverArg = + findArgument(node, method, "android.content.BroadcastReceiver", "receiver") + val filterArg = findArgument(node, method, "android.content.IntentFilter", "filter") + val flagsArg = findArgument(node, method, "int", "flags") + + if (receiverArg == null || receiverArg.isNullLiteral() || filterArg == null) { + return + } + + val evaluator = ConstantEvaluator().allowFieldInitializers() + + val (isProtected, unprotectedActionsList) = + checkIsProtectedReceiverAndReturnUnprotectedActions(filterArg, node, evaluator) + + val flags = evaluator.evaluate(flagsArg) as? Int + + if (!isProtected) { + val actionsList = unprotectedActionsList.joinToString(", ", "", "", -1, "") + val message = "$receiverArg is missing 'RECEIVED_EXPORTED` or 'RECEIVE_NOT_EXPORTED' " + + "flag for unprotected broadcast(s) registered for $actionsList." + if (flagsArg == null) { + context.report( + ISSUE_RECEIVER_EXPORTED_FLAG, node, context.getLocation(node), message) + } else if (flags != null && (flags and RECEIVER_EXPORTED_FLAG_PRESENT_MASK) == 0) { + context.report( + ISSUE_RECEIVER_EXPORTED_FLAG, node, context.getLocation(flagsArg), message) + } + } + + if (DEBUG) { + println(node.asRenderString()) + println("Unprotected Actions: $unprotectedActionsList") + println("Protected: $isProtected") + println("Flags: $flags") + } + } + + /** Finds the first argument of a method that matches the given parameter type and name. */ + private fun findArgument( + node: UCallExpression, + method: PsiMethod, + type: String, + name: String + ): UExpression? { + val psiParameter = method.parameterList.parameters.firstOrNull { + it.type.canonicalText == type && it.name == name + } ?: return null + val argument = node.getArgumentForParameter(psiParameter.parameterIndex()) + return argument?.skipParenthesizedExprDown() + } + + /** + * For the supplied expression (eg. intent filter argument), attempts to find its construction. + * This will be an `IntentFilter()` constructor, an `IntentFilter.create()` call, or `null`. + */ + private fun findIntentFilterConstruction( + expression: UExpression, + node: UCallExpression + ): UCallExpression? { + val resolved = expression.tryResolve() + + if (resolved is PsiVariable) { + val assignment = findLastAssignment(resolved, node) ?: return null + return findIntentFilterConstruction(assignment, node) + } + + if (expression is UParenthesizedExpression) { + return findIntentFilterConstruction(expression.expression, node) + } + + if (expression is UQualifiedReferenceExpression) { + val call = expression.selector as? UCallExpression ?: return null + return if (isReturningContext(call)) { + // eg. filter.apply { addAction("abc") } --> use filter variable. + findIntentFilterConstruction(expression.receiver, node) + } else { + // eg. IntentFilter.create("abc") --> use create("abc") UCallExpression. + findIntentFilterConstruction(call, node) + } + } + + val method = resolved as? PsiMethod ?: return null + return if (isIntentFilterFactoryMethod(method)) { + expression as? UCallExpression + } else { + null + } + } + + private fun isIntentFilterFactoryMethod(method: PsiMethod) = + (method.containingClass?.qualifiedName == "android.content.IntentFilter" && + (method.returnType?.canonicalText == "android.content.IntentFilter" || + method.isConstructor)) + + /** + * Returns true if the given call represents a Kotlin scope function where the return value is + * the context object; see https://kotlinlang.org/docs/scope-functions.html#function-selection. + */ + private fun isReturningContext(node: UCallExpression): Boolean { + val name = getMethodName(node) + if (name == "apply" || name == "also") { + return isScopingFunction(node) + } + return false + } + + /** + * Returns true if the given node appears to be one of the scope functions. Only checks parent + * class; caller should intend that it's actually one of let, with, apply, etc. + */ + private fun isScopingFunction(node: UCallExpression): Boolean { + val called = node.resolve() ?: return true + // See libraries/stdlib/jvm/build/stdlib-declarations.json + return called.containingClass?.qualifiedName == "kotlin.StandardKt__StandardKt" + } + + inner class ActionCollectorVisitor( + start: Collection, + val functionCall: UCallExpression, + val evaluator: ConstantEvaluator, + ) : DataFlowAnalyzer(start) { + private var finished = false + var intentFilterEscapesScope = false; private set + val actions = mutableSetOf() + + override fun argument(call: UCallExpression, reference: UElement) { + // TODO: Remove this temporary fix for DataFlowAnalyzer bug (ag/15787550): + if (reference !in call.valueArguments) return + val methodNames = super@RegisterReceiverFlagDetector.getApplicableMethodNames() + when { + finished -> return + // We've reached the registerReceiver*() call in question. + call == functionCall -> finished = true + // The filter 'intentFilterEscapesScope' to a method which could modify it. + methodNames != null && getMethodName(call)!! !in methodNames -> + intentFilterEscapesScope = true + } + } + + // Fixed in b/199163915: DataFlowAnalyzer doesn't call this for Kotlin properties. + override fun field(field: UElement) { + if (!finished) intentFilterEscapesScope = true + } + + override fun receiver(call: UCallExpression) { + if (!finished && getMethodName(call) == "addAction") { + val actionArg = call.getArgumentForParameter(0) + if (actionArg != null) { + val action = evaluator.evaluate(actionArg) as? String + if (action != null) actions.add(action) + } + } + } + } + + companion object { + const val DEBUG = false + + private const val RECEIVER_EXPORTED = 0x2 + private const val RECEIVER_NOT_EXPORTED = 0x4 + private const val RECEIVER_EXPORTED_FLAG_PRESENT_MASK = + RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED + + @JvmField + val ISSUE_RECEIVER_EXPORTED_FLAG: Issue = Issue.create( + id = "UnspecifiedRegisterReceiverFlag", + briefDescription = "Missing `registerReceiver()` exported flag", + explanation = """ + Apps targeting Android T (SDK 33) and higher must specify either `RECEIVER_EXPORTED` \ + or `RECEIVER_NOT_EXPORTED` when registering a broadcast receiver, unless the \ + receiver is only registered for protected system broadcast actions. + """, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + RegisterReceiverFlagDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val PROTECTED_BROADCASTS = listOf( + "android.intent.action.SCREEN_OFF", + "android.intent.action.SCREEN_ON", + "android.intent.action.USER_PRESENT", + "android.intent.action.TIME_SET", + "android.intent.action.TIME_TICK", + "android.intent.action.TIMEZONE_CHANGED", + "android.intent.action.DATE_CHANGED", + "android.intent.action.PRE_BOOT_COMPLETED", + "android.intent.action.BOOT_COMPLETED", + "android.intent.action.PACKAGE_INSTALL", + "android.intent.action.PACKAGE_ADDED", + "android.intent.action.PACKAGE_REPLACED", + "android.intent.action.MY_PACKAGE_REPLACED", + "android.intent.action.PACKAGE_REMOVED", + "android.intent.action.PACKAGE_REMOVED_INTERNAL", + "android.intent.action.PACKAGE_FULLY_REMOVED", + "android.intent.action.PACKAGE_CHANGED", + "android.intent.action.PACKAGE_FULLY_LOADED", + "android.intent.action.PACKAGE_ENABLE_ROLLBACK", + "android.intent.action.CANCEL_ENABLE_ROLLBACK", + "android.intent.action.ROLLBACK_COMMITTED", + "android.intent.action.PACKAGE_RESTARTED", + "android.intent.action.PACKAGE_DATA_CLEARED", + "android.intent.action.PACKAGE_FIRST_LAUNCH", + "android.intent.action.PACKAGE_NEEDS_INTEGRITY_VERIFICATION", + "android.intent.action.PACKAGE_NEEDS_VERIFICATION", + "android.intent.action.PACKAGE_VERIFIED", + "android.intent.action.PACKAGES_SUSPENDED", + "android.intent.action.PACKAGES_UNSUSPENDED", + "android.intent.action.PACKAGES_SUSPENSION_CHANGED", + "android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY", + "android.intent.action.DISTRACTING_PACKAGES_CHANGED", + "android.intent.action.ACTION_PREFERRED_ACTIVITY_CHANGED", + "android.intent.action.UID_REMOVED", + "android.intent.action.QUERY_PACKAGE_RESTART", + "android.intent.action.CONFIGURATION_CHANGED", + "android.intent.action.SPLIT_CONFIGURATION_CHANGED", + "android.intent.action.LOCALE_CHANGED", + "android.intent.action.APPLICATION_LOCALE_CHANGED", + "android.intent.action.BATTERY_CHANGED", + "android.intent.action.BATTERY_LEVEL_CHANGED", + "android.intent.action.BATTERY_LOW", + "android.intent.action.BATTERY_OKAY", + "android.intent.action.ACTION_POWER_CONNECTED", + "android.intent.action.ACTION_POWER_DISCONNECTED", + "android.intent.action.ACTION_SHUTDOWN", + "android.intent.action.CHARGING", + "android.intent.action.DISCHARGING", + "android.intent.action.DEVICE_STORAGE_LOW", + "android.intent.action.DEVICE_STORAGE_OK", + "android.intent.action.DEVICE_STORAGE_FULL", + "android.intent.action.DEVICE_STORAGE_NOT_FULL", + "android.intent.action.NEW_OUTGOING_CALL", + "android.intent.action.REBOOT", + "android.intent.action.DOCK_EVENT", + "android.intent.action.THERMAL_EVENT", + "android.intent.action.MASTER_CLEAR_NOTIFICATION", + "android.intent.action.USER_ADDED", + "android.intent.action.USER_REMOVED", + "android.intent.action.USER_STARTING", + "android.intent.action.USER_STARTED", + "android.intent.action.USER_STOPPING", + "android.intent.action.USER_STOPPED", + "android.intent.action.USER_BACKGROUND", + "android.intent.action.USER_FOREGROUND", + "android.intent.action.USER_SWITCHED", + "android.intent.action.USER_INITIALIZE", + "android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION", + "android.intent.action.DOMAINS_NEED_VERIFICATION", + "android.intent.action.OVERLAY_ADDED", + "android.intent.action.OVERLAY_CHANGED", + "android.intent.action.OVERLAY_REMOVED", + "android.intent.action.OVERLAY_PRIORITY_CHANGED", + "android.intent.action.MY_PACKAGE_SUSPENDED", + "android.intent.action.MY_PACKAGE_UNSUSPENDED", + "android.os.action.POWER_SAVE_MODE_CHANGED", + "android.os.action.DEVICE_IDLE_MODE_CHANGED", + "android.os.action.POWER_SAVE_WHITELIST_CHANGED", + "android.os.action.POWER_SAVE_TEMP_WHITELIST_CHANGED", + "android.os.action.POWER_SAVE_MODE_CHANGED_INTERNAL", + "android.os.action.LOW_POWER_STANDBY_ENABLED_CHANGED", + "android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED", + "android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED", + "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL", + "android.app.action.ENTER_CAR_MODE", + "android.app.action.EXIT_CAR_MODE", + "android.app.action.ENTER_CAR_MODE_PRIORITIZED", + "android.app.action.EXIT_CAR_MODE_PRIORITIZED", + "android.app.action.ENTER_DESK_MODE", + "android.app.action.EXIT_DESK_MODE", + "android.app.action.NEXT_ALARM_CLOCK_CHANGED", + "android.app.action.USER_ADDED", + "android.app.action.USER_REMOVED", + "android.app.action.USER_STARTED", + "android.app.action.USER_STOPPED", + "android.app.action.USER_SWITCHED", + "android.app.action.BUGREPORT_SHARING_DECLINED", + "android.app.action.BUGREPORT_FAILED", + "android.app.action.BUGREPORT_SHARE", + "android.app.action.SHOW_DEVICE_MONITORING_DIALOG", + "android.intent.action.PENDING_INCIDENT_REPORTS_CHANGED", + "android.intent.action.INCIDENT_REPORT_READY", + "android.appwidget.action.APPWIDGET_UPDATE_OPTIONS", + "android.appwidget.action.APPWIDGET_DELETED", + "android.appwidget.action.APPWIDGET_DISABLED", + "android.appwidget.action.APPWIDGET_ENABLED", + "android.appwidget.action.APPWIDGET_HOST_RESTORED", + "android.appwidget.action.APPWIDGET_RESTORED", + "android.appwidget.action.APPWIDGET_ENABLE_AND_UPDATE", + "android.os.action.SETTING_RESTORED", + "android.app.backup.intent.CLEAR", + "android.app.backup.intent.INIT", + "android.bluetooth.intent.DISCOVERABLE_TIMEOUT", + "android.bluetooth.adapter.action.STATE_CHANGED", + "android.bluetooth.adapter.action.SCAN_MODE_CHANGED", + "android.bluetooth.adapter.action.DISCOVERY_STARTED", + "android.bluetooth.adapter.action.DISCOVERY_FINISHED", + "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED", + "android.bluetooth.adapter.action.BLUETOOTH_ADDRESS_CHANGED", + "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.device.action.UUID", + "android.bluetooth.device.action.MAS_INSTANCE", + "android.bluetooth.device.action.ALIAS_CHANGED", + "android.bluetooth.device.action.FOUND", + "android.bluetooth.device.action.CLASS_CHANGED", + "android.bluetooth.device.action.ACL_CONNECTED", + "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED", + "android.bluetooth.device.action.ACL_DISCONNECTED", + "android.bluetooth.device.action.NAME_CHANGED", + "android.bluetooth.device.action.BOND_STATE_CHANGED", + "android.bluetooth.device.action.NAME_FAILED", + "android.bluetooth.device.action.PAIRING_REQUEST", + "android.bluetooth.device.action.PAIRING_CANCEL", + "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY", + "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL", + "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST", + "android.bluetooth.device.action.SDP_RECORD", + "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED", + "android.bluetooth.devicepicker.action.LAUNCH", + "android.bluetooth.devicepicker.action.DEVICE_SELECTED", + "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED", + "android.bluetooth.action.CSIS_DEVICE_AVAILABLE", + "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE", + "android.bluetooth.mapmce.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED", + "android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY", + "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY", + "android.bluetooth.mapmce.profile.action.MESSAGE_READ_STATUS_CHANGED", + "android.bluetooth.mapmce.profile.action.MESSAGE_DELETED_STATUS_CHANGED", + "android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED", + "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED", + "android.bluetooth.action.LE_AUDIO_CONF_CHANGED", + "android.bluetooth.action.LE_AUDIO_GROUP_NODE_STATUS_CHANGED", + "android.bluetooth.action.LE_AUDIO_GROUP_STATUS_CHANGED", + "android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.pbapclient.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED", + "android.btopp.intent.action.INCOMING_FILE_NOTIFICATION", + "android.btopp.intent.action.USER_CONFIRMATION_TIMEOUT", + "android.btopp.intent.action.LIST", + "android.btopp.intent.action.OPEN_OUTBOUND", + "android.btopp.intent.action.HIDE_COMPLETE", + "android.btopp.intent.action.CONFIRM", + "android.btopp.intent.action.HIDE", + "android.btopp.intent.action.RETRY", + "android.btopp.intent.action.OPEN", + "android.btopp.intent.action.OPEN_INBOUND", + "android.btopp.intent.action.TRANSFER_COMPLETE", + "android.btopp.intent.action.ACCEPT", + "android.btopp.intent.action.DECLINE", + "com.android.bluetooth.gatt.REFRESH_BATCHED_SCAN", + "com.android.bluetooth.pbap.authchall", + "com.android.bluetooth.pbap.userconfirmtimeout", + "com.android.bluetooth.pbap.authresponse", + "com.android.bluetooth.pbap.authcancelled", + "com.android.bluetooth.sap.USER_CONFIRM_TIMEOUT", + "com.android.bluetooth.sap.action.DISCONNECT_ACTION", + "android.hardware.display.action.WIFI_DISPLAY_STATUS_CHANGED", + "android.hardware.usb.action.USB_STATE", + "android.hardware.usb.action.USB_PORT_CHANGED", + "android.hardware.usb.action.USB_ACCESSORY_ATTACHED", + "android.hardware.usb.action.USB_ACCESSORY_DETACHED", + "android.hardware.usb.action.USB_ACCESSORY_HANDSHAKE", + "android.hardware.usb.action.USB_DEVICE_ATTACHED", + "android.hardware.usb.action.USB_DEVICE_DETACHED", + "android.intent.action.HEADSET_PLUG", + "android.media.action.HDMI_AUDIO_PLUG", + "android.media.action.MICROPHONE_MUTE_CHANGED", + "android.media.action.SPEAKERPHONE_STATE_CHANGED", + "android.media.AUDIO_BECOMING_NOISY", + "android.media.RINGER_MODE_CHANGED", + "android.media.VIBRATE_SETTING_CHANGED", + "android.media.VOLUME_CHANGED_ACTION", + "android.media.MASTER_VOLUME_CHANGED_ACTION", + "android.media.MASTER_MUTE_CHANGED_ACTION", + "android.media.MASTER_MONO_CHANGED_ACTION", + "android.media.MASTER_BALANCE_CHANGED_ACTION", + "android.media.SCO_AUDIO_STATE_CHANGED", + "android.media.ACTION_SCO_AUDIO_STATE_UPDATED", + "android.intent.action.MEDIA_REMOVED", + "android.intent.action.MEDIA_UNMOUNTED", + "android.intent.action.MEDIA_CHECKING", + "android.intent.action.MEDIA_NOFS", + "android.intent.action.MEDIA_MOUNTED", + "android.intent.action.MEDIA_SHARED", + "android.intent.action.MEDIA_UNSHARED", + "android.intent.action.MEDIA_BAD_REMOVAL", + "android.intent.action.MEDIA_UNMOUNTABLE", + "android.intent.action.MEDIA_EJECT", + "android.net.conn.CAPTIVE_PORTAL", + "android.net.conn.CONNECTIVITY_CHANGE", + "android.net.conn.CONNECTIVITY_CHANGE_IMMEDIATE", + "android.net.conn.DATA_ACTIVITY_CHANGE", + "android.net.conn.RESTRICT_BACKGROUND_CHANGED", + "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED", + "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED", + "android.net.nsd.STATE_CHANGED", + "android.se.omapi.action.SECURE_ELEMENT_STATE_CHANGED", + "android.nfc.action.ADAPTER_STATE_CHANGED", + "android.nfc.action.PREFERRED_PAYMENT_CHANGED", + "android.nfc.action.TRANSACTION_DETECTED", + "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC", + "com.android.nfc.action.LLCP_UP", + "com.android.nfc.action.LLCP_DOWN", + "com.android.nfc.cardemulation.action.CLOSE_TAP_DIALOG", + "com.android.nfc.handover.action.ALLOW_CONNECT", + "com.android.nfc.handover.action.DENY_CONNECT", + "com.android.nfc.handover.action.TIMEOUT_CONNECT", + "com.android.nfc_extras.action.RF_FIELD_ON_DETECTED", + "com.android.nfc_extras.action.RF_FIELD_OFF_DETECTED", + "com.android.nfc_extras.action.AID_SELECTED", + "android.btopp.intent.action.WHITELIST_DEVICE", + "android.btopp.intent.action.STOP_HANDOVER_TRANSFER", + "android.nfc.handover.intent.action.HANDOVER_SEND", + "android.nfc.handover.intent.action.HANDOVER_SEND_MULTIPLE", + "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER", + "android.net.action.CLEAR_DNS_CACHE", + "android.intent.action.PROXY_CHANGE", + "android.os.UpdateLock.UPDATE_LOCK_CHANGED", + "android.intent.action.DREAMING_STARTED", + "android.intent.action.DREAMING_STOPPED", + "android.intent.action.ANY_DATA_STATE", + "com.android.server.stats.action.TRIGGER_COLLECTION", + "com.android.server.WifiManager.action.START_SCAN", + "com.android.server.WifiManager.action.START_PNO", + "com.android.server.WifiManager.action.DELAYED_DRIVER_STOP", + "com.android.server.WifiManager.action.DEVICE_IDLE", + "com.android.server.action.REMOTE_BUGREPORT_SHARING_ACCEPTED", + "com.android.server.action.REMOTE_BUGREPORT_SHARING_DECLINED", + "com.android.internal.action.EUICC_FACTORY_RESET", + "com.android.server.usb.ACTION_OPEN_IN_APPS", + "com.android.server.am.DELETE_DUMPHEAP", + "com.android.server.net.action.SNOOZE_WARNING", + "com.android.server.net.action.SNOOZE_RAPID", + "com.android.server.wifi.ACTION_SHOW_SET_RANDOMIZATION_DETAILS", + "com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_APP", + "com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_APP", + "com.android.server.wifi.action.NetworkSuggestion.USER_DISMISSED", + "com.android.server.wifi.action.CarrierNetwork.USER_ALLOWED_CARRIER", + "com.android.server.wifi.action.CarrierNetwork.USER_DISALLOWED_CARRIER", + "com.android.server.wifi.action.CarrierNetwork.USER_DISMISSED", + "com.android.server.wifi.ConnectToNetworkNotification.USER_DISMISSED_NOTIFICATION", + "com.android.server.wifi.ConnectToNetworkNotification.CONNECT_TO_NETWORK", + "com.android.server.wifi.ConnectToNetworkNotification.PICK_WIFI_NETWORK", + "com.android.server.wifi.ConnectToNetworkNotification.PICK_NETWORK_AFTER_FAILURE", + "com.android.server.wifi.wakeup.DISMISS_NOTIFICATION", + "com.android.server.wifi.wakeup.OPEN_WIFI_PREFERENCES", + "com.android.server.wifi.wakeup.OPEN_WIFI_SETTINGS", + "com.android.server.wifi.wakeup.TURN_OFF_WIFI_WAKE", + "android.net.wifi.WIFI_STATE_CHANGED", + "android.net.wifi.WIFI_AP_STATE_CHANGED", + "android.net.wifi.WIFI_CREDENTIAL_CHANGED", + "android.net.wifi.aware.action.WIFI_AWARE_STATE_CHANGED", + "android.net.wifi.aware.action.WIFI_AWARE_RESOURCE_CHANGED", + "android.net.wifi.rtt.action.WIFI_RTT_STATE_CHANGED", + "android.net.wifi.SCAN_RESULTS", + "android.net.wifi.RSSI_CHANGED", + "android.net.wifi.STATE_CHANGE", + "android.net.wifi.LINK_CONFIGURATION_CHANGED", + "android.net.wifi.CONFIGURED_NETWORKS_CHANGE", + "android.net.wifi.action.NETWORK_SETTINGS_RESET", + "android.net.wifi.action.PASSPOINT_DEAUTH_IMMINENT", + "android.net.wifi.action.PASSPOINT_ICON", + "android.net.wifi.action.PASSPOINT_OSU_PROVIDERS_LIST", + "android.net.wifi.action.PASSPOINT_SUBSCRIPTION_REMEDIATION", + "android.net.wifi.action.PASSPOINT_LAUNCH_OSU_VIEW", + "android.net.wifi.action.REFRESH_USER_PROVISIONING", + "android.net.wifi.action.WIFI_NETWORK_SUGGESTION_POST_CONNECTION", + "android.net.wifi.action.WIFI_SCAN_AVAILABILITY_CHANGED", + "android.net.wifi.supplicant.CONNECTION_CHANGE", + "android.net.wifi.supplicant.STATE_CHANGE", + "android.net.wifi.p2p.STATE_CHANGED", + "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE", + "android.net.wifi.p2p.THIS_DEVICE_CHANGED", + "android.net.wifi.p2p.PEERS_CHANGED", + "android.net.wifi.p2p.CONNECTION_STATE_CHANGE", + "android.net.wifi.p2p.action.WIFI_P2P_PERSISTENT_GROUPS_CHANGED", + "android.net.conn.TETHER_STATE_CHANGED", + "android.net.conn.INET_CONDITION_ACTION", + "android.net.conn.NETWORK_CONDITIONS_MEASURED", + "android.net.scoring.SCORE_NETWORKS", + "android.net.scoring.SCORER_CHANGED", + "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE", + "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE", + "android.intent.action.AIRPLANE_MODE", + "android.intent.action.ADVANCED_SETTINGS", + "android.intent.action.APPLICATION_RESTRICTIONS_CHANGED", + "com.android.server.adb.WIRELESS_DEBUG_PAIRED_DEVICES", + "com.android.server.adb.WIRELESS_DEBUG_PAIRING_RESULT", + "com.android.server.adb.WIRELESS_DEBUG_STATUS", + "android.intent.action.ACTION_IDLE_MAINTENANCE_START", + "android.intent.action.ACTION_IDLE_MAINTENANCE_END", + "com.android.server.ACTION_TRIGGER_IDLE", + "android.intent.action.HDMI_PLUGGED", + "android.intent.action.PHONE_STATE", + "android.intent.action.SUB_DEFAULT_CHANGED", + "android.location.PROVIDERS_CHANGED", + "android.location.MODE_CHANGED", + "android.location.action.GNSS_CAPABILITIES_CHANGED", + "android.net.proxy.PAC_REFRESH", + "android.telecom.action.DEFAULT_DIALER_CHANGED", + "android.provider.action.DEFAULT_SMS_PACKAGE_CHANGED", + "android.provider.action.SMS_MMS_DB_CREATED", + "android.provider.action.SMS_MMS_DB_LOST", + "android.intent.action.CONTENT_CHANGED", + "android.provider.Telephony.MMS_DOWNLOADED", + "android.content.action.PERMISSION_RESPONSE_RECEIVED", + "android.content.action.REQUEST_PERMISSION", + "android.nfc.handover.intent.action.HANDOVER_STARTED", + "android.nfc.handover.intent.action.TRANSFER_DONE", + "android.nfc.handover.intent.action.TRANSFER_PROGRESS", + "android.nfc.handover.intent.action.TRANSFER_DONE", + "android.intent.action.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED", + "android.intent.action.ACTION_SET_RADIO_CAPABILITY_DONE", + "android.intent.action.ACTION_SET_RADIO_CAPABILITY_FAILED", + "android.internal.policy.action.BURN_IN_PROTECTION", + "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED", + "android.app.action.RESET_PROTECTION_POLICY_CHANGED", + "android.app.action.DEVICE_OWNER_CHANGED", + "android.app.action.MANAGED_USER_CREATED", + "android.intent.action.ANR", + "android.intent.action.CALL", + "android.intent.action.CALL_PRIVILEGED", + "android.intent.action.DROPBOX_ENTRY_ADDED", + "android.intent.action.INPUT_METHOD_CHANGED", + "android.intent.action.internal_sim_state_changed", + "android.intent.action.LOCKED_BOOT_COMPLETED", + "android.intent.action.PRECISE_CALL_STATE", + "android.intent.action.SUBSCRIPTION_PHONE_STATE", + "android.intent.action.USER_INFO_CHANGED", + "android.intent.action.USER_UNLOCKED", + "android.intent.action.WALLPAPER_CHANGED", + "android.app.action.DEVICE_POLICY_MANAGER_STATE_CHANGED", + "android.app.action.CHOOSE_PRIVATE_KEY_ALIAS", + "android.app.action.DEVICE_ADMIN_DISABLED", + "android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED", + "android.app.action.DEVICE_ADMIN_ENABLED", + "android.app.action.LOCK_TASK_ENTERING", + "android.app.action.LOCK_TASK_EXITING", + "android.app.action.NOTIFY_PENDING_SYSTEM_UPDATE", + "android.app.action.ACTION_PASSWORD_CHANGED", + "android.app.action.ACTION_PASSWORD_EXPIRING", + "android.app.action.ACTION_PASSWORD_FAILED", + "android.app.action.ACTION_PASSWORD_SUCCEEDED", + "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION", + "com.android.server.ACTION_PROFILE_OFF_DEADLINE", + "com.android.server.ACTION_TURN_PROFILE_ON_NOTIFICATION", + "android.intent.action.MANAGED_PROFILE_ADDED", + "android.intent.action.MANAGED_PROFILE_UNLOCKED", + "android.intent.action.MANAGED_PROFILE_REMOVED", + "android.app.action.MANAGED_PROFILE_PROVISIONED", + "android.bluetooth.adapter.action.BLE_STATE_CHANGED", + "com.android.bluetooth.map.USER_CONFIRM_TIMEOUT", + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT", + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY", + "android.content.jobscheduler.JOB_DELAY_EXPIRED", + "android.content.syncmanager.SYNC_ALARM", + "android.media.INTERNAL_RINGER_MODE_CHANGED_ACTION", + "android.media.STREAM_DEVICES_CHANGED_ACTION", + "android.media.STREAM_MUTE_CHANGED_ACTION", + "android.net.sip.SIP_SERVICE_UP", + "android.nfc.action.ADAPTER_STATE_CHANGED", + "android.os.action.CHARGING", + "android.os.action.DISCHARGING", + "android.search.action.SEARCHABLES_CHANGED", + "android.security.STORAGE_CHANGED", + "android.security.action.TRUST_STORE_CHANGED", + "android.security.action.KEYCHAIN_CHANGED", + "android.security.action.KEY_ACCESS_CHANGED", + "android.telecom.action.NUISANCE_CALL_STATUS_CHANGED", + "android.telecom.action.PHONE_ACCOUNT_REGISTERED", + "android.telecom.action.PHONE_ACCOUNT_UNREGISTERED", + "android.telecom.action.POST_CALL", + "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION", + "android.telephony.action.CARRIER_CONFIG_CHANGED", + "android.telephony.action.DEFAULT_SUBSCRIPTION_CHANGED", + "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED", + "android.telephony.action.SECRET_CODE", + "android.telephony.action.SHOW_VOICEMAIL_NOTIFICATION", + "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED", + "com.android.bluetooth.btservice.action.ALARM_WAKEUP", + "com.android.server.action.NETWORK_STATS_POLL", + "com.android.server.action.NETWORK_STATS_UPDATED", + "com.android.server.timedetector.NetworkTimeUpdateService.action.POLL", + "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY", + "com.android.settings.location.MODE_CHANGING", + "com.android.settings.bluetooth.ACTION_DISMISS_PAIRING", + "com.android.settings.network.DELETE_SUBSCRIPTION", + "com.android.settings.network.SWITCH_TO_SUBSCRIPTION", + "com.android.settings.wifi.action.NETWORK_REQUEST", + "NotificationManagerService.TIMEOUT", + "NotificationHistoryDatabase.CLEANUP", + "ScheduleConditionProvider.EVALUATE", + "EventConditionProvider.EVALUATE", + "SnoozeHelper.EVALUATE", + "wifi_scan_available", + "action.cne.started", + "android.content.jobscheduler.JOB_DEADLINE_EXPIRED", + "android.intent.action.ACTION_UNSOL_RESPONSE_OEM_HOOK_RAW", + "android.net.conn.CONNECTIVITY_CHANGE_SUPL", + "android.os.action.LIGHT_DEVICE_IDLE_MODE_CHANGED", + "android.os.storage.action.VOLUME_STATE_CHANGED", + "android.os.storage.action.DISK_SCANNED", + "com.android.server.action.UPDATE_TWILIGHT_STATE", + "com.android.server.action.RESET_TWILIGHT_AUTO", + "com.android.server.device_idle.STEP_IDLE_STATE", + "com.android.server.device_idle.STEP_LIGHT_IDLE_STATE", + "com.android.server.Wifi.action.TOGGLE_PNO", + "intent.action.ACTION_RF_BAND_INFO", + "android.intent.action.MEDIA_RESOURCE_GRANTED", + "android.app.action.NETWORK_LOGS_AVAILABLE", + "android.app.action.SECURITY_LOGS_AVAILABLE", + "android.app.action.COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED", + "android.app.action.INTERRUPTION_FILTER_CHANGED", + "android.app.action.INTERRUPTION_FILTER_CHANGED_INTERNAL", + "android.app.action.NOTIFICATION_POLICY_CHANGED", + "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED", + "android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED", + "android.os.action.ACTION_EFFECTS_SUPPRESSOR_CHANGED", + "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED", + "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED", + "android.app.action.NOTIFICATION_LISTENER_ENABLED_CHANGED", + "android.app.action.APP_BLOCK_STATE_CHANGED", + "android.permission.GET_APP_GRANTED_URI_PERMISSIONS", + "android.permission.CLEAR_APP_GRANTED_URI_PERMISSIONS", + "android.intent.action.DYNAMIC_SENSOR_CHANGED", + "android.accounts.LOGIN_ACCOUNTS_CHANGED", + "android.accounts.action.ACCOUNT_REMOVED", + "android.accounts.action.VISIBLE_ACCOUNTS_CHANGED", + "com.android.sync.SYNC_CONN_STATUS_CHANGED", + "android.net.sip.action.SIP_INCOMING_CALL", + "com.android.phone.SIP_ADD_PHONE", + "android.net.sip.action.SIP_REMOVE_PROFILE", + "android.net.sip.action.SIP_SERVICE_UP", + "android.net.sip.action.SIP_CALL_OPTION_CHANGED", + "android.net.sip.action.START_SIP", + "android.bluetooth.adapter.action.BLE_ACL_CONNECTED", + "android.bluetooth.adapter.action.BLE_ACL_DISCONNECTED", + "android.bluetooth.input.profile.action.HANDSHAKE", + "android.bluetooth.input.profile.action.REPORT", + "android.intent.action.TWILIGHT_CHANGED", + "com.android.server.fingerprint.ACTION_LOCKOUT_RESET", + "android.net.wifi.PASSPOINT_ICON_RECEIVED", + "com.android.server.notification.CountdownConditionProvider", + "android.server.notification.action.ENABLE_NAS", + "android.server.notification.action.DISABLE_NAS", + "android.server.notification.action.LEARNMORE_NAS", + "com.android.internal.location.ALARM_WAKEUP", + "com.android.internal.location.ALARM_TIMEOUT", + "android.intent.action.GLOBAL_BUTTON", + "android.intent.action.MANAGED_PROFILE_AVAILABLE", + "android.intent.action.MANAGED_PROFILE_UNAVAILABLE", + "com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK", + "android.intent.action.PROFILE_ACCESSIBLE", + "android.intent.action.PROFILE_INACCESSIBLE", + "com.android.server.retaildemo.ACTION_RESET_DEMO", + "android.intent.action.DEVICE_LOCKED_CHANGED", + "com.android.content.pm.action.CAN_INTERACT_ACROSS_PROFILES_CHANGED", + "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED", + "com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION", + "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED", + "android.content.pm.action.SESSION_COMMITTED", + "android.os.action.USER_RESTRICTIONS_CHANGED", + "android.media.tv.action.PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT", + "android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED", + "android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED", + "android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED", + "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER", + "com.android.intent.action.timezone.RULES_UPDATE_OPERATION", + "com.android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK", + "android.intent.action.GET_RESTRICTION_ENTRIES", + "android.telephony.euicc.action.OTA_STATUS_CHANGED", + "android.app.action.PROFILE_OWNER_CHANGED", + "android.app.action.TRANSFER_OWNERSHIP_COMPLETE", + "android.app.action.AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE", + "android.app.action.STATSD_STARTED", + "com.android.server.biometrics.fingerprint.ACTION_LOCKOUT_RESET", + "com.android.server.biometrics.face.ACTION_LOCKOUT_RESET", + "android.intent.action.DOCK_IDLE", + "android.intent.action.DOCK_ACTIVE", + "android.content.pm.action.SESSION_UPDATED", + "android.settings.action.GRAYSCALE_CHANGED", + "com.android.server.jobscheduler.GARAGE_MODE_ON", + "com.android.server.jobscheduler.GARAGE_MODE_OFF", + "com.android.server.jobscheduler.FORCE_IDLE", + "com.android.server.jobscheduler.UNFORCE_IDLE", + "android.provider.action.DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL", + "android.intent.action.DEVICE_CUSTOMIZATION_READY", + "android.app.action.RESET_PROTECTION_POLICY_CHANGED", + "com.android.internal.intent.action.BUGREPORT_REQUESTED", + "android.scheduling.action.REBOOT_READY", + "android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED", + "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED", + "android.app.action.SHOW_NEW_USER_DISCLAIMER", + "android.telecom.action.CURRENT_TTY_MODE_CHANGED", + "android.intent.action.SERVICE_STATE", + "android.intent.action.RADIO_TECHNOLOGY", + "android.intent.action.EMERGENCY_CALLBACK_MODE_CHANGED", + "android.intent.action.EMERGENCY_CALL_STATE_CHANGED", + "android.intent.action.SIG_STR", + "android.intent.action.ANY_DATA_STATE", + "android.intent.action.DATA_STALL_DETECTED", + "android.intent.action.SIM_STATE_CHANGED", + "android.intent.action.USER_ACTIVITY_NOTIFICATION", + "android.telephony.action.SHOW_NOTICE_ECM_BLOCK_OTHERS", + "android.intent.action.ACTION_MDN_STATE_CHANGED", + "android.telephony.action.SERVICE_PROVIDERS_UPDATED", + "android.provider.Telephony.SIM_FULL", + "com.android.internal.telephony.carrier_key_download_alarm", + "com.android.internal.telephony.data-restart-trysetup", + "com.android.internal.telephony.data-stall", + "com.android.internal.telephony.provisioning_apn_alarm", + "android.intent.action.DATA_SMS_RECEIVED", + "android.provider.Telephony.SMS_RECEIVED", + "android.provider.Telephony.SMS_DELIVER", + "android.provider.Telephony.SMS_REJECTED", + "android.provider.Telephony.WAP_PUSH_DELIVER", + "android.provider.Telephony.WAP_PUSH_RECEIVED", + "android.provider.Telephony.SMS_CB_RECEIVED", + "android.provider.action.SMS_EMERGENCY_CB_RECEIVED", + "android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED", + "android.provider.Telephony.SECRET_CODE", + "com.android.internal.stk.command", + "com.android.internal.stk.session_end", + "com.android.internal.stk.icc_status_change", + "com.android.internal.stk.alpha_notify", + "com.android.internal.telephony.CARRIER_SIGNAL_REDIRECTED", + "com.android.internal.telephony.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED", + "com.android.internal.telephony.CARRIER_SIGNAL_PCO_VALUE", + "com.android.internal.telephony.CARRIER_SIGNAL_RESET", + "com.android.internal.telephony.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE", + "com.android.internal.telephony.PROVISION", + "com.android.internal.telephony.ACTION_LINE1_NUMBER_ERROR_DETECTED", + "com.android.internal.provider.action.VOICEMAIL_SMS_RECEIVED", + "com.android.intent.isim_refresh", + "com.android.ims.ACTION_RCS_SERVICE_AVAILABLE", + "com.android.ims.ACTION_RCS_SERVICE_UNAVAILABLE", + "com.android.ims.ACTION_RCS_SERVICE_DIED", + "com.android.ims.ACTION_PRESENCE_CHANGED", + "com.android.ims.ACTION_PUBLISH_STATUS_CHANGED", + "com.android.ims.IMS_SERVICE_UP", + "com.android.ims.IMS_SERVICE_DOWN", + "com.android.ims.IMS_INCOMING_CALL", + "com.android.ims.internal.uce.UCE_SERVICE_UP", + "com.android.ims.internal.uce.UCE_SERVICE_DOWN", + "com.android.imsconnection.DISCONNECTED", + "com.android.intent.action.IMS_FEATURE_CHANGED", + "com.android.intent.action.IMS_CONFIG_CHANGED", + "android.telephony.ims.action.WFC_IMS_REGISTRATION_ERROR", + "com.android.phone.vvm.omtp.sms.REQUEST_SENT", + "com.android.phone.vvm.ACTION_VISUAL_VOICEMAIL_SERVICE_EVENT", + "com.android.internal.telephony.CARRIER_VVM_PACKAGE_INSTALLED", + "com.android.cellbroadcastreceiver.GET_LATEST_CB_AREA_INFO", + "com.android.internal.telephony.ACTION_CARRIER_CERTIFICATE_DOWNLOAD", + "com.android.internal.telephony.action.COUNTRY_OVERRIDE", + "com.android.internal.telephony.OPEN_DEFAULT_SMS_APP", + "com.android.internal.telephony.ACTION_TEST_OVERRIDE_CARRIER_ID", + "android.telephony.action.SIM_CARD_STATE_CHANGED", + "android.telephony.action.SIM_APPLICATION_STATE_CHANGED", + "android.telephony.action.SIM_SLOT_STATUS_CHANGED", + "android.telephony.action.SUBSCRIPTION_CARRIER_IDENTITY_CHANGED", + "android.telephony.action.SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED", + "android.telephony.action.TOGGLE_PROVISION", + "android.telephony.action.NETWORK_COUNTRY_CHANGED", + "android.telephony.action.PRIMARY_SUBSCRIPTION_LIST_CHANGED", + "android.telephony.action.MULTI_SIM_CONFIG_CHANGED", + "android.telephony.action.CARRIER_SIGNAL_RESET", + "android.telephony.action.CARRIER_SIGNAL_PCO_VALUE", + "android.telephony.action.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE", + "android.telephony.action.CARRIER_SIGNAL_REDIRECTED", + "android.telephony.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED", + "com.android.phone.settings.CARRIER_PROVISIONING", + "com.android.phone.settings.TRIGGER_CARRIER_PROVISIONING", + "com.android.internal.telephony.ACTION_VOWIFI_ENABLED", + "android.telephony.action.ANOMALY_REPORTED", + "android.intent.action.SUBSCRIPTION_INFO_RECORD_ADDED", + "android.intent.action.ACTION_MANAGED_ROAMING_IND", + "android.telephony.ims.action.RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE", + "android.safetycenter.action.REFRESH_SAFETY_SOURCES", + "android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED", + "android.app.action.DEVICE_POLICY_RESOURCE_UPDATED", + "android.intent.action.SHOW_FOREGROUND_SERVICE_MANAGER", + "android.service.autofill.action.DELAYED_FILL", + "android.app.action.PROVISIONING_COMPLETED", + "android.app.action.LOST_MODE_LOCATION_UPDATE", + "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED", + "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT", + "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED", + "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED", + "android.bluetooth.headsetclient.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.headsetclient.profile.action.AUDIO_STATE_CHANGED", + "android.bluetooth.headsetclient.profile.action.AG_EVENT", + "android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED", + "android.bluetooth.headsetclient.profile.action.RESULT", + "android.bluetooth.headsetclient.profile.action.LAST_VTAG", + "android.bluetooth.headsetclient.profile.action.NETWORK_SERVICE_STATE_CHANGED", + "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.hearingaid.profile.action.PLAYING_STATE_CHANGED", + "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED", + "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.a2dp.profile.action.ACTIVE_DEVICE_CHANGED", + "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED", + "android.bluetooth.a2dp.profile.action.CODEC_CONFIG_CHANGED", + "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.a2dp-sink.profile.action.PLAYING_STATE_CHANGED", + "android.bluetooth.a2dp-sink.profile.action.AUDIO_CONFIG_CHANGED", + "android.bluetooth.avrcp-controller.profile.action.BROWSE_CONNECTION_STATE_CHANGED", + "android.bluetooth.avrcp-controller.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST", + "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT", + "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.input.profile.action.IDLE_TIME_CHANGED", + "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED", + "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS", + "android.bluetooth.hiddevice.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.map.profile.action.CONNECTION_STATE_CHANGED", + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT", + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY", + "android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED", + "android.bluetooth.action.TETHERING_STATE_CHANGED", + "com.android.internal.action.EUICC_REMOVE_INVISIBLE_SUBSCRIPTIONS", + "android.net.ConnectivityService.action.PKT_CNT_SAMPLE_INTERVAL_ELAPSED", + "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION", + "com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM" + ) + } +} 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 000000000000..06c098df385d --- /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 = 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 = + 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 = 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>) (Class) Foo.class. + * This is needed for when the arguments of the conflict (eg. when there is List> and + * class type is Class) (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 = 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 = 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 = 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 = 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 000000000000..0826e8e74431 --- /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, + val clazz: String, + val name: String, + val parameters: List +) { + constructor( + clazz: String, + name: String, + parameters: List + ) : 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 000000000000..f92826316be4 --- /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 = + 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 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", "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 000000000000..d90f3e31baf9 --- /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 = 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 000000000000..e72f38416310 --- /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 = 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 000000000000..a70644ab8532 --- /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 = 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/RegisterReceiverFlagDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt new file mode 100644 index 000000000000..b76342a81972 --- /dev/null +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/RegisterReceiverFlagDetectorTest.kt @@ -0,0 +1,560 @@ +/* + * 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 RegisterReceiverFlagDetectorTest : LintDetectorTest() { + + override fun getDetector(): Detector = RegisterReceiverFlagDetector() + + override fun getIssues(): List = listOf( + RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testProtectedBroadcast() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testProtectedBroadcastCreate() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = + IntentFilter.create(Intent.ACTION_BATTERY_CHANGED, "foo/bar"); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testMultipleProtectedBroadcasts() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + filter.addAction(Intent.ACTION_BATTERY_LOW); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testSubsequentFilterModification() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + filter.addAction(Intent.ACTION_BATTERY_LOW); + filter.addAction(Intent.ACTION_BATTERY_OKAY); + context.registerReceiver(receiver, filter); + filter.addAction("querty"); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.java:13: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testNullReceiver() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context) { + IntentFilter filter = new IntentFilter("qwerty"); + context.registerReceiver(null, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testExportedFlagPresent() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter("qwerty"); + context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testNotExportedFlagPresent() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter("qwerty"); + context.registerReceiver(receiver, filter, + Context.RECEIVER_NOT_EXPORTED); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testFlagArgumentAbsent() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter("qwerty"); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testExportedFlagsAbsent() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter("qwerty"); + context.registerReceiver(receiver, filter, 0); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter, 0); + ~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testExportedFlagVariable() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter("qwerty"); + var flags = Context.RECEIVER_EXPORTED; + context.registerReceiver(receiver, filter, flags); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testUnknownFilter() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver, + IntentFilter filter) { + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.java:9: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testFilterEscapes() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + updateFilter(filter); + context.registerReceiver(receiver, filter); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.java:10: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testInlineFilter() { + lint().files( + java( + """ + package test.pkg; + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + import android.content.IntentFilter; + public class TestClass1 { + public void testMethod(Context context, BroadcastReceiver receiver) { + context.registerReceiver(receiver, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInlineFilterApply() { + lint().files( + kotlin( + """ + package test.pkg + import android.content.BroadcastReceiver + import android.content.Context + import android.content.Intent + import android.content.IntentFilter + class TestClass1 { + fun test(context: Context, receiver: BroadcastReceiver) { + context.registerReceiver(receiver, + IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { + addAction("qwerty") + }) + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.kt:8: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, + ^ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testFilterVariableApply() { + lint().files( + kotlin( + """ + package test.pkg + import android.content.BroadcastReceiver + import android.content.Context + import android.content.Intent + import android.content.IntentFilter + class TestClass1 { + fun test(context: Context, receiver: BroadcastReceiver) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { + addAction("qwerty") + } + context.registerReceiver(receiver, filter) + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.kt:11: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testFilterVariableApply2() { + lint().files( + kotlin( + """ + package test.pkg + import android.content.BroadcastReceiver + import android.content.Context + import android.content.Intent + import android.content.IntentFilter + class TestClass1 { + fun test(context: Context, receiver: BroadcastReceiver) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { + addAction(Intent.ACTION_BATTERY_OKAY) + } + context.registerReceiver(receiver, filter.apply { + addAction("qwerty") + }) + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.kt:11: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter.apply { + ^ + 0 errors, 1 warnings + """.trimIndent()) + } + + fun testFilterComplexChain() { + lint().files( + kotlin( + """ + package test.pkg + import android.content.BroadcastReceiver + import android.content.Context + import android.content.Intent + import android.content.IntentFilter + class TestClass1 { + fun test(context: Context, receiver: BroadcastReceiver) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED).apply { + addAction(Intent.ACTION_BATTERY_OKAY) + } + val filter2 = filter + val filter3 = filter2.apply { + addAction(Intent.ACTION_BATTERY_LOW) + } + context.registerReceiver(receiver, filter3) + val filter4 = filter3.apply { + addAction("qwerty") + } + context.registerReceiver(receiver, filter4) + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass1.kt:19: Warning: Missing RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag [UnspecifiedRegisterReceiverFlag] + context.registerReceiver(receiver, filter4) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.trimIndent()) + } + + private val broadcastReceiverStub: TestFile = java( + """ + package android.content; + public class BroadcastReceiver { + // Stub + } + """ + ).indented() + + private val contextStub: TestFile = java( + """ + package android.content; + public class Context { + public static final int RECEIVER_EXPORTED = 0x2; + public static final int RECEIVER_NOT_EXPORTED = 0x4; + @Nullable + public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver, + IntentFilter filter, + @RegisterReceiverFlags int flags); + } + """ + ).indented() + + private val intentStub: TestFile = java( + """ + package android.content; + public class Intent { + public static final String ACTION_BATTERY_CHANGED = + "android.intent.action.BATTERY_CHANGED"; + public static final String ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW"; + public static final String ACTION_BATTERY_OKAY = + "android.intent.action.BATTERY_OKAY"; + } + """ + ).indented() + + private val intentFilterStub: TestFile = java( + """ + package android.content; + public class IntentFilter { + public IntentFilter() { + // Stub + } + public IntentFilter(String action) { + // Stub + } + public IntentFilter(String action, String dataType) { + // Stub + } + public static IntentFilter create(String action, String dataType) { + return null; + } + public final void addAction(String action) { + // Stub + } + } + """ + ).indented() + + private val stubs = arrayOf(broadcastReceiverStub, contextStub, intentStub, intentFilterStub) +} 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 000000000000..e686695ca804 --- /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 = 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 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 list = new ArrayList(); + 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 list = new ArrayList(); + 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 list = new ArrayList(); + List ans = p.readParcelableList(list, null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \ + API usage [UnsafeParcelApi] + List 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 list = new ArrayList(); + List 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 ans = p.readSparseArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\ + usage [UnsafeParcelApi] + SparseArray 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 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 ans = b.getParcelableArrayList("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi] + ArrayList 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 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 ans = b.getSparseParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi] + SparseArray 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 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 readParcelable(ClassLoader loader) { return null; } + public Parcelable[] readParcelableArray(ClassLoader loader) { return null; } + public Parcelable.Creator readParcelableCreator(ClassLoader loader) { return null; } + public List readParcelableList(List list, ClassLoader cl) { return null; } + public Serializable readSerializable() { return null; } + public SparseArray readSparseArray(ClassLoader loader) { return null; } + + // Replacements + public T[] readArray(ClassLoader loader, Class clazz) { return null; } + public ArrayList readArrayList(ClassLoader loader, Class clazz) { return null; } + public HashMap readHashMap(ClassLoader loader, Class clazzKey, Class clazzValue) { return null; } + public void readList(List outVal, ClassLoader loader, Class clazz) {} + public void readMap(Map outVal, ClassLoader loader, Class clazzKey, Class clazzValue) {} + public T readParcelable(ClassLoader loader, Class clazz) { return null; } + public T[] readParcelableArray(ClassLoader loader, Class clazz) { return null; } + public Parcelable.Creator readParcelableCreator(ClassLoader loader, Class clazz) { return null; } + public List readParcelableList(List list, ClassLoader cl, Class clazz) { return null; } + public T readSerializable(ClassLoader loader, Class clazz) { return null; } + public SparseArray readSparseArray(ClassLoader loader, Class 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 getParcelable(String key) { return null; } + public ArrayList getParcelableArrayList(String key) { return null; } + public Parcelable[] getParcelableArray(String key) { return null; } + public SparseArray getSparseParcelableArray(String key) { return null; } + + // Replacements + public T getParcelable(String key, Class clazz) { return null; } + public ArrayList getParcelableArrayList(String key, Class clazz) { return null; } + public T[] getParcelableArray(String key, Class clazz) { return null; } + public SparseArray getSparseParcelableArray(String key, Class 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 getParcelableExtra(String name) { return null; } + + // Replacements + public T getParcelableExtra(String name, Class clazz) { return null; } + + } + """ + ).indented(), + java( + """ + package android.util; + public class SparseArray implements Cloneable {} + """ + ).indented(), + ) + + // 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/global/Android.bp b/tools/lint/global/Android.bp new file mode 100644 index 000000000000..3756abea2330 --- /dev/null +++ b/tools/lint/global/Android.bp @@ -0,0 +1,57 @@ +// 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: "AndroidGlobalLintChecker", + srcs: ["checks/src/main/java/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + static_libs: ["AndroidCommonLint"], + kotlincflags: ["-Xjvm-default=all"], + dist: { + targets: ["droid"], + }, +} + +java_test_host { + name: "AndroidGlobalLintCheckerTest", + // TODO(b/239881504): Since this test was written, Android + // Lint was updated, and now includes classes that were + // compiled for java 15. The soong build doesn't support + // java 15 yet, so we can't compile against "lint". Disable + // the test until java 15 is supported. + enabled: false, + srcs: ["checks/src/test/java/**/*.kt"], + static_libs: [ + "AndroidGlobalLintChecker", + "junit", + "lint", + "lint_tests", + ], + test_options: { + unit_test: true, + }, +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt new file mode 100644 index 000000000000..b377d503f318 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt @@ -0,0 +1,48 @@ +/* + * 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.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.EnforcePermissionHelperDetector +import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class AndroidGlobalIssueRegistry : IssueRegistry() { + override val issues = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + SimpleManualPermissionEnforcementDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, + ) + + 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 = "repsonsible-apis@google.com" + ) +} \ No newline at end of file diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt new file mode 100644 index 000000000000..227cdcdc2fec --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt @@ -0,0 +1,52 @@ +/* + * 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.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +/** + * Abstract class for detectors that look for methods implementing + * generated AIDL interface stubs + */ +abstract class AidlImplementationDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + val interfaceName = getContainingAidlInterface(node) + .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return + val body = (node.uastBody as? UBlockExpression) ?: return + visitAidlMethod(context, node, interfaceName, body) + } + } + + abstract fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression, + ) +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt new file mode 100644 index 000000000000..8ee3763e5079 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt @@ -0,0 +1,73 @@ +/* + * 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.aidl + +const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission" +const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission" +const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced" + +val AIDL_PERMISSION_ANNOTATIONS = listOf( + ANNOTATION_ENFORCE_PERMISSION, + ANNOTATION_REQUIRES_NO_PERMISSION, + ANNOTATION_PERMISSION_MANUALLY_ENFORCED +) + +const val IINTERFACE_INTERFACE = "android.os.IInterface" + +/** + * If a non java (e.g. c++) backend is enabled, the @EnforcePermission + * annotation cannot be used. At time of writing, the mechanism + * is not implemented for non java backends. + * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled) + * rather than hard coding this list? + */ +val EXCLUDED_CPP_INTERFACES = listOf( + "AdbTransportType", + "FingerprintAndPairDevice", + "IAdbCallback", + "IAdbManager", + "PairDevice", + "IStatsBootstrapAtomService", + "StatsBootstrapAtom", + "StatsBootstrapAtomValue", + "FixedSizeArrayExample", + "PlaybackTrackMetadata", + "RecordTrackMetadata", + "SinkMetadata", + "SourceMetadata", + "IUpdateEngineStable", + "IUpdateEngineStableCallback", + "AudioCapabilities", + "ConfidenceLevel", + "ModelParameter", + "ModelParameterRange", + "Phrase", + "PhraseRecognitionEvent", + "PhraseRecognitionExtra", + "PhraseSoundModel", + "Properties", + "RecognitionConfig", + "RecognitionEvent", + "RecognitionMode", + "RecognitionStatus", + "SoundModel", + "SoundModelType", + "Status", + "IThermalService", + "IPowerManager", + "ITunerResourceManager" +) diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt new file mode 100644 index 000000000000..bba819cd9096 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt @@ -0,0 +1,255 @@ +/* + * 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.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.AnnotationInfo +import com.android.tools.lint.detector.api.AnnotationOrigin +import com.android.tools.lint.detector.api.AnnotationUsageInfo +import com.android.tools.lint.detector.api.AnnotationUsageType +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.ConstantEvaluator +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.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +/** + * Lint Detector that ensures that any method overriding a method annotated + * with @EnforcePermission is also annotated with the exact same annotation. + * The intent is to surface the effective permission checks to the service + * implementations. + * + * This is done with 2 mechanisms: + * 1. Visit any annotation usage, to ensure that any derived class will have + * the correct annotation on each methods. This is for the top to bottom + * propagation. + * 2. Visit any annotation, to ensure that if a method is annotated, it has + * its ancestor also annotated. This is to avoid having an annotation on a + * Java method without the corresponding annotation on the AIDL interface. + */ +class EnforcePermissionDetector : Detector(), SourceCodeScanner { + + val BINDER_CLASS = "android.os.Binder" + val JAVA_OBJECT = "java.lang.Object" + + override fun applicableAnnotations(): List { + return listOf(ANNOTATION_ENFORCE_PERMISSION) + } + + override fun getApplicableUastTypes(): List> { + return listOf(UAnnotation::class.java) + } + + private fun areAnnotationsEquivalent( + context: JavaContext, + anno1: PsiAnnotation, + anno2: PsiAnnotation + ): Boolean { + if (anno1.qualifiedName != anno2.qualifiedName) { + return false + } + val attr1 = anno1.parameterList.attributes + val attr2 = anno2.parameterList.attributes + if (attr1.size != attr2.size) { + return false + } + for (i in attr1.indices) { + if (attr1[i].name != attr2[i].name) { + return false + } + val value1 = attr1[i].value + val value2 = attr2[i].value + if (value1 == null && value2 == null) { + continue + } + if (value1 == null || value2 == null) { + return false + } + val v1 = ConstantEvaluator.evaluate(context, value1) + val v2 = ConstantEvaluator.evaluate(context, value2) + if (v1 != v2) { + return false + } + } + return true + } + + private fun compareMethods( + context: JavaContext, + element: UElement, + overridingMethod: PsiMethod, + overriddenMethod: PsiMethod, + checkEquivalence: Boolean = true + ) { + val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val location = context.getLocation(element) + val overridingClass = overridingMethod.parent as PsiClass + val overriddenClass = overriddenMethod.parent as PsiClass + val overridingName = "${overridingClass.name}.${overridingMethod.name}" + val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" + if (overridingAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is annotated with @EnforcePermission. The same annotation must be used " + + "on $overridingName" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (overriddenAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is not annotated with @EnforcePermission. The same annotation must be " + + "used on $overriddenName. Did you forget to annotate the AIDL definition?" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (checkEquivalence && !areAnnotationsEquivalent( + context, overridingAnnotation, overriddenAnnotation)) { + val msg = "The method $overridingName is annotated with " + + "${overridingAnnotation.text} which differs from the overridden " + + "method $overriddenName: ${overriddenAnnotation.text}. The same " + + "annotation must be used for both methods." + context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) + } + } + + private fun compareClasses( + context: JavaContext, + element: UElement, + newClass: PsiClass, + extendedClass: PsiClass, + checkEquivalence: Boolean = true + ) { + val newAnnotation = newClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val extendedAnnotation = extendedClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + + val location = context.getLocation(element) + val newClassName = newClass.qualifiedName + val extendedClassName = extendedClass.qualifiedName + if (newAnnotation == null) { + val msg = "The class $newClassName extends the class $extendedClassName which " + + "is annotated with @EnforcePermission. The same annotation must be used " + + "on $newClassName." + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (extendedAnnotation == null) { + val msg = "The class $newClassName extends the class $extendedClassName which " + + "is not annotated with @EnforcePermission. The same annotation must be used " + + "on $extendedClassName. Did you forget to annotate the AIDL definition?" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (checkEquivalence && !areAnnotationsEquivalent( + context, newAnnotation, extendedAnnotation)) { + val msg = "The class $newClassName is annotated with ${newAnnotation.text} " + + "which differs from the parent class $extendedClassName: " + + "${extendedAnnotation.text}. The same annotation must be used for " + + "both classes." + context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) + } + } + + override fun visitAnnotationUsage( + context: JavaContext, + element: UElement, + annotationInfo: AnnotationInfo, + usageInfo: AnnotationUsageInfo + ) { + if (usageInfo.type == AnnotationUsageType.EXTENDS) { + val newClass = element.sourcePsi?.parent?.parent as PsiClass + val extendedClass: PsiClass = usageInfo.referenced as PsiClass + compareClasses(context, element, newClass, extendedClass) + } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && + annotationInfo.origin == AnnotationOrigin.METHOD) { + val overridingMethod = element.sourcePsi as PsiMethod + val overriddenMethod = usageInfo.referenced as PsiMethod + compareMethods(context, element, overridingMethod, overriddenMethod) + } + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) { + return + } + val method = node.uastParent as? UMethod + val klass = node.uastParent as? UClass + if (klass != null) { + val newClass = klass as PsiClass + val extendedClass = newClass.getSuperClass() + if (extendedClass != null && extendedClass.qualifiedName != JAVA_OBJECT) { + // The equivalence check can be skipped, if both classes are + // annotated, it will be verified by visitAnnotationUsage. + compareClasses(context, klass, newClass, + extendedClass, checkEquivalence = false) + } + } else if (method != null) { + val overridingMethod = method as PsiMethod + val parents = overridingMethod.findSuperMethods() + for (overriddenMethod in parents) { + // The equivalence check can be skipped, if both methods are + // annotated, it will be verified by visitAnnotationUsage. + compareMethods(context, method, overridingMethod, + overriddenMethod, checkEquivalence = false) + } + } + } + } + } + + companion object { + val EXPLANATION = """ + The @EnforcePermission annotation is used to indicate that the underlying binder code + has already verified the caller's permissions before calling the appropriate method. The + verification code is usually generated by the AIDL compiler, which also takes care of + annotating the generated Java code. + + In order to surface that information to platform developers, the same annotation must be + used on the implementation class or methods. + """ + + val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MissingEnforcePermissionAnnotation", + briefDescription = "Missing @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MismatchingEnforcePermissionAnnotation", + briefDescription = "Incorrect @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt new file mode 100644 index 000000000000..f1b634898ec9 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -0,0 +1,139 @@ +/* + * 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.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.hasPermissionNameAnnotation +import com.google.android.lint.isPermissionMethodCall +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.visitor.AbstractUastVisitor + +/** + * Helper class that facilitates the creation of lint auto fixes + * + * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks + * that should be migrated to @EnforcePermission(allOf={...}) + * + * TODO: handle anyOf style annotations + */ +data class EnforcePermissionFix( + val locations: List, + val permissionNames: List +) { + val annotation: String + get() { + val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } + val annotationParameter = + if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions + return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" + } + + companion object { + /** + * conditionally constructs EnforcePermissionFix from a UCallExpression + * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null + */ + fun fromCallExpression( + context: JavaContext, + callExpression: UCallExpression + ): EnforcePermissionFix? = + if (isPermissionMethodCall(callExpression)) { + EnforcePermissionFix( + listOf(getPermissionCheckLocation(context, callExpression)), + getPermissionCheckValues(callExpression) + ) + } else null + + + fun compose(individuals: List): EnforcePermissionFix = + EnforcePermissionFix( + individuals.flatMap { it.locations }, + individuals.flatMap { it.permissionNames } + ) + + /** + * Given a permission check, get its proper location + * so that a lint fix can remove the entire expression + */ + private fun getPermissionCheckLocation( + context: JavaContext, + callExpression: UCallExpression + ): + Location { + val javaPsi = callExpression.javaPsi!! + return Location.create( + context.file, + javaPsi.containingFile?.text, + javaPsi.textRange.startOffset, + // unfortunately the element doesn't include the ending semicolon + javaPsi.textRange.endOffset + 1 + ) + } + + /** + * Given a @PermissionMethod, find arguments annotated with @PermissionName + * and pull out the permission value(s) being used. Also evaluates nested calls + * to @PermissionMethod(s) in the given method's body. + */ + private fun getPermissionCheckValues( + callExpression: UCallExpression + ): List { + if (!isPermissionMethodCall(callExpression)) return emptyList() + + val result = mutableSetOf() // protect against duplicate permission values + val visitedCalls = mutableSetOf() // don't visit the same call twice + val bfsQueue = ArrayDeque(listOf(callExpression)) + + // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available + // source code for @PermissionName(s). + while (bfsQueue.isNotEmpty()) { + val current = bfsQueue.removeFirst() + visitedCalls.add(current) + result.addAll(findPermissions(current)) + + current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (isPermissionMethodCall(node) && node !in visitedCalls) { + bfsQueue.add(node) + } + return false + } + }) + } + + return result.toList() + } + + private fun findPermissions( + callExpression: UCallExpression, + ): List { + val indices = callExpression.resolve()?.getUMethod() + ?.uastParameters + ?.filter(::hasPermissionNameAnnotation) + ?.mapNotNull { it.sourcePsi?.parameterIndex() } + ?: emptyList() + + return indices.mapNotNull { + callExpression.getArgumentForParameter(it)?.evaluateString() + } + } + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt new file mode 100644 index 000000000000..3c2ea1db0ad6 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -0,0 +1,130 @@ +/* + * 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.aidl + +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.intellij.psi.PsiElement +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UDeclarationsExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return + + val targetExpression = "super.${node.name}$HELPER_SUFFIX()" + + val body = node.uastBody as? UBlockExpression + if (body == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + ) + return + } + + val firstExpression = body.expressions.firstOrNull() + if (firstExpression == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + ) + return + } + + val firstExpressionSource = firstExpression.asSourceString() + .filterNot(Char::isWhitespace) + + if (firstExpressionSource != targetExpression) { + val locationTarget = getLocationTarget(firstExpression) + val expressionLocation = context.getLocation(locationTarget) + val indent = " ".repeat(expressionLocation.start?.column ?: 0) + + val fix = fix() + .replace() + .range(expressionLocation) + .beginning() + .with("$targetExpression;\n\n$indent") + .reformat(true) + .autoFix() + .build() + + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + "Method must start with $targetExpression", + fix + ) + } + } + } + + companion object { + private const val HELPER_SUFFIX = "_enforcePermission" + + private const val EXPLANATION = """ + When @EnforcePermission is applied, the AIDL compiler generates a Stub method to do the + permission check called yourMethodName$HELPER_SUFFIX. + + You must call this method as the first expression in your implementation. + """ + + val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( + id = "MissingEnforcePermissionHelper", + briefDescription = """Missing permission-enforcing method call in AIDL method + |annotated with @EnforcePermission""".trimMargin(), + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionHelperDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + /** + * handles an edge case with UDeclarationsExpression, where sourcePsi is null, + * resulting in an incorrect Location if used directly + */ + private fun getLocationTarget(firstExpression: UExpression): PsiElement? { + if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi + if (firstExpression is UDeclarationsExpression) { + return firstExpression.declarations.firstOrNull()?.sourcePsi + } + return null + } + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt new file mode 100644 index 000000000000..250ca78bae5e --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt @@ -0,0 +1,48 @@ +/* + * 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.aidl + +import com.google.android.lint.CLASS_STUB +import com.intellij.psi.PsiAnonymousClass +import org.jetbrains.uast.UMethod + +/** + * Given a UMethod, determine if this method is + * an entrypoint to an interface generated by AIDL, + * returning the interface name if so + */ +fun getContainingAidlInterface(node: UMethod): String? { + if (!isInClassCalledStub(node)) return null + for (superMethod in node.findSuperMethods()) { + for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements + ?: continue) { + if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { + return superMethod.containingClass?.name + } + } + } + return null +} + +private fun isInClassCalledStub(node: UMethod): Boolean { + (node.containingClass as? PsiAnonymousClass)?.let { + return it.baseClassReference.referenceName == CLASS_STUB + } + return node.containingClass?.extendsList?.referenceElements?.any { + it.referenceName == CLASS_STUB + } ?: false +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt new file mode 100644 index 000000000000..4c0cbe7b3adf --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt @@ -0,0 +1,150 @@ +/* + * 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.aidl + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression + +/** + * Looks for methods implementing generated AIDL interface stubs + * that can have simple permission checks migrated to + * @EnforcePermission annotations + * + * TODO: b/242564870 (enable parse and autoFix of .aidl files) + */ +@Suppress("UnstableApiUsage") +class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { + override fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression + ) { + val fix = accumulateSimplePermissionCheckFixes(body, context) ?: return + + val javaRemoveFixes = fix.locations.map { + fix() + .replace() + .reformat(true) + .range(it) + .with("") + .autoFix() + .build() + } + + val javaAnnotateFix = fix() + .annotate(fix.annotation) + .range(context.getLocation(node)) + .autoFix() + .build() + + context.report( + ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION, + fix.locations.last(), + "$interfaceName permission check can be converted to @EnforcePermission annotation", + fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix) + ) + } + + /** + * Walk the expressions in the method, looking for simple permission checks. + * + * If a single permission check is found at the beginning of the method, + * this should be migrated to @EnforcePermission(value). + * + * If multiple consecutive permission checks are found, + * these should be migrated to @EnforcePermission(allOf={value1, value2, ...}) + * + * As soon as something other than a permission check is encountered, stop looking, + * as some other business logic is happening that prevents an automated fix. + */ + private fun accumulateSimplePermissionCheckFixes( + methodBody: UBlockExpression, + context: JavaContext + ): + EnforcePermissionFix? { + val singleFixes = mutableListOf() + for (expression in methodBody.expressions) { + singleFixes.add(getPermissionCheckFix(expression, context) ?: break) + } + return when (singleFixes.size) { + 0 -> null + 1 -> singleFixes[0] + else -> EnforcePermissionFix.compose(singleFixes) + } + } + + /** + * If an expression boils down to a permission check, return + * the helper for creating a lint auto fix to @EnforcePermission + */ + private fun getPermissionCheckFix(startingExpression: UElement?, context: JavaContext): + EnforcePermissionFix? { + return when (startingExpression) { + is UQualifiedReferenceExpression -> getPermissionCheckFix( + startingExpression.selector, context + ) + + is UIfExpression -> getPermissionCheckFix(startingExpression.condition, context) + + is UCallExpression -> return EnforcePermissionFix + .fromCallExpression(context, startingExpression) + + else -> null + } + } + + companion object { + + private val EXPLANATION = """ + Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission + annotation to declare the permissions to be enforced. The verification code is then + generated by the AIDL compiler, which also takes care of annotating the generated java + code. + + This reduces the risk of bugs around these permission checks (that often become vulnerabilities). + It also enables easier auditing and review. + + Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto) + """.trimIndent() + + @JvmField + val ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION = Issue.create( + id = "SimpleManualPermissionEnforcement", + briefDescription = "Manual permission check can be @EnforcePermission annotation", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + SimpleManualPermissionEnforcementDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = false, // TODO: enable once b/241171714 is resolved + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt new file mode 100644 index 000000000000..3c1d1e8e8a59 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt @@ -0,0 +1,259 @@ +/* + * 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.aidl + +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 EnforcePermissionDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionDetector() + + override fun getIssues(): List = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testDoesNotDetectIssuesCorrectAnnotationOnClass() { + lint().files(java( + """ + package test.pkg; + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public class TestClass1 extends IFoo.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass2 extends IFooMethod.Stub { + @Override + @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDetectIssuesMismatchingAnnotationOnClass() { + lint().files(java( + """ + package test.pkg; + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public class TestClass3 extends IFoo.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \ +annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ +which differs from the parent class IFoo.Stub: \ +@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \ +same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation] +public class TestClass3 extends IFoo.Stub { + ~~~~~~~~~ +1 errors, 0 warnings""".addLineContinuation()) + } + + fun testDetectIssuesMismatchingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass4 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \ +annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ +which differs from the overridden method Stub.testMethod: \ +@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \ +annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ +1 errors, 0 warnings""".addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnClass() { + lint().files(java( + """ + package test.pkg; + public class TestClass5 extends IFoo.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \ +the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \ +used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation] +public class TestClass5 extends IFoo.Stub { + ~~~~~~~~~ +1 errors, 0 warnings""".addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass6 extends IFooMethod.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \ +overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \ +annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ +1 errors, 0 warnings""".addLineContinuation()) + } + + fun testDetectIssuesExtraAnnotationMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass7 extends IBar.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod \ +overrides the method Stub.testMethod which is not annotated with @EnforcePermission. The same \ +annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? \ +[MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ +1 errors, 0 warnings""".addLineContinuation()) + } + + fun testDetectIssuesExtraAnnotationInterface() { + lint().files(java( + """ + package test.pkg; + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public class TestClass8 extends IBar.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect("""src/test/pkg/TestClass8.java:2: Error: The class test.pkg.TestClass8 \ +extends the class IBar.Stub which is not annotated with @EnforcePermission. The same annotation \ +must be used on IBar.Stub. Did you forget to annotate the AIDL definition? \ +[MissingEnforcePermissionAnnotation] +@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) +^ +1 errors, 0 warnings""".addLineContinuation()) + } + + /* Stubs */ + + // A service with permission annotation on the class. + private val interfaceIFooStub: TestFile = java( + """ + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public interface IFoo { + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public static abstract class Stub extends android.os.Binder implements IFoo { + @Override + public void testMethod() {} + } + public void testMethod(); + } + """ + ).indented() + + // A service with permission annotation on the method. + private val interfaceIFooMethodStub: TestFile = java( + """ + public interface IFooMethod { + public static abstract class Stub extends android.os.Binder implements IFooMethod { + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + } + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod(); + } + """ + ).indented() + + // A service without any permission annotation. + private val interfaceIBarStub: TestFile = java( + """ + public interface IBar { + public static abstract class Stub extends android.os.Binder implements IBar { + @Override + public void testMethod() {} + } + public void testMethod(); + } + """ + ).indented() + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, interfaceIBarStub, + manifestPermissionStub, enforcePermissionAnnotationStub) + + // 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/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt new file mode 100644 index 000000000000..31e484628a04 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt @@ -0,0 +1,159 @@ +/* +* 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.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask + +class EnforcePermissionHelperDetectorTest : LintDetectorTest() { + override fun getDetector() = EnforcePermissionHelperDetector() + override fun getIssues() = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testFirstExpressionIsFunctionCall() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: + @@ -8 +8 + + super.test_enforcePermission(); + + + """ + ) + } + + fun testFirstExpressionIsVariableDeclaration() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + String foo = "bar"; + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...: + @@ -8 +8 + + super.test_enforcePermission(); + + + """ + ) + } + + fun testMethodIsEmpty() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException {} + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testOkay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + companion object { + val stubs = arrayOf(aidlStub, contextStub, binderStub) + } +} + + + diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt new file mode 100644 index 000000000000..150fc264506f --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt @@ -0,0 +1,371 @@ +/* + * 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.aidl + +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 SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector() + override fun getIssues(): List = listOf( + SimpleManualPermissionEnforcementDetector + .ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + """ + ) + } + + fun testAnonClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + """ + ) + } + + fun testConstantEvaluation() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + } + } + """ + ).indented(), + *stubs, + manifestStub + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + """ + ) + } + + fun testAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingOrSelfPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + """ + ) + } + + fun testPrecedingExpressions() { + lint().files( + java( + """ + import android.os.Binder; + import android.test.ITest; + public class Foo extends ITest.Stub { + private mContext Context; + @Override + public void test() throws android.os.RemoteException { + long uid = Binder.getCallingUid(); + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testPermissionHelper() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + """ + ) + } + + fun testPermissionHelperAllOf() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 16: Annotate with @EnforcePermission: + @@ -13 +13 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"}) + @@ -15 +16 + - helper(); + - mContext.enforceCallingOrSelfPermission("FOO", "foo"); + """ + ) + } + + + fun testPermissionHelperNested() { + lint().skipTestModes(TestMode.PARENTHESIZED).files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void helperHelper() { + helper("android.permission.WRITE_CONTACTS"); + } + + @android.content.pm.PermissionMethod + private void helper(@android.content.pm.PermissionName String extraPermission) { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 19: Annotate with @EnforcePermission: + @@ -17 +17 + + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"}) + @@ -19 +20 + - helperHelper(); + """ + ) + } + + + + companion object { + val stubs = arrayOf( + aidlStub, + contextStub, + binderStub, + permissionMethodStub, + permissionNameStub + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt new file mode 100644 index 000000000000..bd6b1952847c --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -0,0 +1,80 @@ +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFile + +val aidlStub: TestFile = java( + """ + package android.test; + public interface ITest extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements android.test.ITest {} + public void test() throws android.os.RemoteException; + } + """ +).indented() + +val contextStub: TestFile = java( + """ + package android.content; + public class Context { + @android.content.pm.PermissionMethod + public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} + } + """ +).indented() + +val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static int getCallingUid() {} + } + """ +).indented() + +val permissionMethodStub: TestFile = java( +""" + package android.content.pm; + + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({METHOD}) + public @interface PermissionMethod {} + """ +).indented() + +val permissionNameStub: TestFile = java( +""" + package android.content.pm; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.LOCAL_VARIABLE; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) + public @interface PermissionName {} + """ +).indented() + +val manifestStub: TestFile = java( + """ + package android; + + public final class Manifest { + public static final class permission { + public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; + } + } + """.trimIndent() +) \ No newline at end of file -- cgit v1.2.3-59-g8ed1b