Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8afcc7d5 authored by Azhara Assanova's avatar Azhara Assanova
Browse files

Moved issues/IssueRegistry to mimic AS convention + README for AndroidFrameworkLintChecker

Moved CallingIdentityTokenDetector's issues from IssueRegistry to itself
since this is the pattern used for built-in Android Studio checks.

Wrote README.md on how to build a lint report and how to add new lint
checks.

Bug: 192925086
Test: m out/soong/.intermediates/frameworks/base/services/accessibility/services.accessibility/android_common/lint/lint-report.html
Change-Id: I6be3d49e5eacc34cf60db125a52ae3da9174be4a
parent f0de8d34
Loading
Loading
Loading
Loading

tools/lint/README.md

0 → 100644
+51 −0
Original line number Original line Diff line number Diff line
# Android Framework Lint Checker

Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any
`services.XXX` module) and results will be automatically reported on CLs on gerrit.

## How to add new lint checks

1. Write your detector with its issues and put it into
   `checks/src/main/java/com/google/android/lint`.
2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field.
3. Write unit tests for your detector in one file and put it into
   `checks/test/java/com/google/android/lint`.
4. Done! Your lint checks should be applied in lint report builds for modules that include
   `AndroidFrameworkLintChecker`.

## How to run lint against your module

1. Add the following `lint` attribute to the module definition, e.g. `services.autofill`:
```
java_library_static {
    name: "services.autofill",
    ...
    lint: {
        extra_check_modules: ["AndroidFrameworkLintChecker"],
    },
}
```
2. Run the following command to verify that the report is being correctly built:
```
m out/soong/.intermediates/frameworks/base/services/autofill/services.autofill/android_common/lint/lint-report.html
```
   (Lint report can be found in the same path, i.e. `out/../lint-report.html`)
3. Now lint issues should appear on gerrit!

**Notes:**

- Lint report will not be produced if you just build the module, i.e. `m services.autofill` will not
  build the lint report.
- If you want to build lint reports for more than 1 module and they depend on a common module, e.g.
  `platform_service_defaults`, you can add the `lint` property to that common module instead of
  adding it in every module.

## Documentation

- [go/android-security-lint-checks](http://go/android-security-lint-checks) - presentation about
  this module
- [Android Lint Docs](http://googlesamples.github.io/android-custom-lint-rules/)
- [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/)
- [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi)
- [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast)
- [IntelliJ plugin for viewing PSI tree of files](https://plugins.jetbrains.com/plugin/227-psiviewer)
+47 −0
Original line number Original line Diff line number Diff line
/*
 * 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.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
    )

    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"
    )
}
+238 −28
Original line number Original line Diff line number Diff line
@@ -16,24 +16,16 @@


package com.google.android.lint
package com.google.android.lint


import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_NESTED_CLEAR_IDENTITY_CALLS
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_NON_FINAL_TOKEN
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_UNUSED_TOKEN
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageClearIdentityCallNotFollowedByTryFinally
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNestedClearIdentityCallsPrimary
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNestedClearIdentityCallsSecondary
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageNonFinalToken
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageRestoreIdentityCallNotInFinallyBlock
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageUnusedToken
import com.google.android.lint.CallingIdentityTokenIssueRegistry.Companion.getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity
import com.android.tools.lint.client.api.UElementHandler
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.Context
import com.android.tools.lint.detector.api.Detector
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.JavaContext
import com.android.tools.lint.detector.api.Location
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.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethod
import com.intellij.psi.search.PsiSearchScopeUtil
import com.intellij.psi.search.PsiSearchScopeUtil
@@ -54,21 +46,6 @@ import org.jetbrains.uast.getParentOfType
 */
 */
@Suppress("UnstableApiUsage")
@Suppress("UnstableApiUsage")
class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
    private companion object {
        const val CLASS_BINDER = "android.os.Binder"
        const val CLASS_USER_HANDLE = "android.os.UserHandle"

        @JvmField
        val callerAwareMethods = listOf(
                Method.BINDER_GET_CALLING_PID,
                Method.BINDER_GET_CALLING_UID,
                Method.BINDER_GET_CALLING_UID_OR_THROW,
                Method.BINDER_GET_CALLING_USER_HANDLE,
                Method.USER_HANDLE_GET_CALLING_APP_ID,
                Method.USER_HANDLE_GET_CALLING_USER_ID
        )
    }

    /** Map of <Token variable name, Token object> */
    /** Map of <Token variable name, Token object> */
    private val tokensMap = mutableMapOf<String, Token>()
    private val tokensMap = mutableMapOf<String, Token>()


@@ -357,4 +334,237 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
        val location: Location,
        val location: Location,
        val finallyBlock: UElement?
        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)`."
    }
}
}
+0 −272

File deleted.

Preview size limit exceeded, changes collapsed.

+6 −7
Original line number Original line Diff line number Diff line
@@ -26,13 +26,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() {
    override fun getDetector(): Detector = CallingIdentityTokenDetector()
    override fun getDetector(): Detector = CallingIdentityTokenDetector()


    override fun getIssues(): List<Issue> = listOf(
    override fun getIssues(): List<Issue> = listOf(
            CallingIdentityTokenIssueRegistry.ISSUE_UNUSED_TOKEN,
            CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
            CallingIdentityTokenIssueRegistry.ISSUE_NON_FINAL_TOKEN,
            CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
            CallingIdentityTokenIssueRegistry.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
            CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
            CallingIdentityTokenIssueRegistry.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
            CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
            CallingIdentityTokenIssueRegistry
            CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
                    .ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
            CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY
            CallingIdentityTokenIssueRegistry.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY
    )
    )


    /** No issue scenario */
    /** No issue scenario */