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

Commit 4adb2ce4 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add RequiresPermissionDetector" into main

parents 94448b47 c8dc5158
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ 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"
const val CLASS_PERMISSION_CHECKER = "android.content.PermissionChecker"

// Enforce permission APIs
val ENFORCE_PERMISSION_METHODS = listOf(
+1 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.google.android.lint.aidl

const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission"
const val ANNOTATION_REQUIRES_PERMISSION = "android.annotation.RequiresPermission"
const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission"
const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced"

+3 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ 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.PermissionAnnotationDetector
import com.google.android.lint.aidl.RequiresPermissionDetector
import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
import com.google.android.lint.aidl.SimpleRequiresNoPermissionDetector
import com.google.android.lint.flags.CheckFlagsRuleDetector
@@ -34,6 +35,8 @@ class AndroidGlobalIssueRegistry : IssueRegistry() {
            EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION,
            EnforcePermissionDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
            EnforcePermissionDetector.ISSUE_MISUSING_ENFORCE_PERMISSION,
            RequiresPermissionDetector.ISSUE_MISSING_OR_MISMATCHED_REQUIRES_PERMISSION_ANNOTATION,
            RequiresPermissionDetector.ISSUE_INCORRECT_REQUIRES_PERMISSION_PROPAGATION,
            PermissionAnnotationDetector.ISSUE_MISSING_PERMISSION_ANNOTATION,
            SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT,
            SimpleRequiresNoPermissionDetector.ISSUE_SIMPLE_REQUIRES_NO_PERMISSION,
+310 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.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.google.android.lint.CLASS_CONTEXT
import com.google.android.lint.CLASS_PERMISSION_CHECKER
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.visitor.AbstractUastVisitor

/**
 * A lint detector that ensures correctness for `@RequiresPermission` annotations.
 *
 * This detector performs two main functions:
 *
 * 1.  **Override Verification:** For methods that override a super-method with a
 * `@RequiresPermission` annotation (such as an AIDL interface method), it ensures the
 * implementation method has a `@RequiresPermission` annotation that is semantically
 * identical to the one on the super-method.
 * (See `ISSUE_MISSING_OR_MISMATCHED_REQUIRES_PERMISSION_ANNOTATION`)
 *
 * 2.  **Permission Propagation:** For any method that does not override a permission-annotated
 * method, it checks that its `@RequiresPermission` annotation correctly reflects the permissions
 * required by the methods it calls.
 * - It reports an error if the annotation is "too narrow" (i.e., it fails to declare a
 * permission that a called method requires).
 * - It also reports an error if the annotation is "too broad" (i.e., it declares a
 * permission that is not required by any of its callees), to prevent unnecessary
 * permission declarations.
 * (See `ISSUE_INCORRECT_REQUIRES_PERMISSION_PROPAGATION`)
 *
 * The detector correctly handles `allOf` and `anyOf` permission sets and ignores permission checks
 * made within a `Binder.clearCallingIdentity()` block.
 */
class RequiresPermissionDetector : Detector(), SourceCodeScanner {

    override fun getApplicableUastTypes(): List<Class<out UElement>> =
        listOf(UMethod::class.java)

    override fun createUastHandler(context: JavaContext): UElementHandler =
        RequiresPermissionVisitor(context)

    private inner class RequiresPermissionVisitor(
        private val context: JavaContext
    ) : UElementHandler() {
        override fun visitMethod(node: UMethod) {
            if (context.evaluator.isAbstract(node)) {
                return
            }

            val superPermissions = getRequiredPermissionsFromSuper(context, node)
            val declaredPermissions = getRequiredPermissionsFromMethod(context, node)

            val nodeName = (node.uastParent as? PsiClass)?.name + "." + node.name
            if (!superPermissions.isEmpty() && declaredPermissions != superPermissions) {
                context.report(
                    ISSUE_MISSING_OR_MISMATCHED_REQUIRES_PERMISSION_ANNOTATION,
                    node,
                    context.getNameLocation(node),
                    "Method `$nodeName` must have an equivalent @RequiresPermission annotation to the one in " +
                            "the super method. Expected: $superPermissions but found: $declaredPermissions."
                )
                return
            }

            val enforcementVisitor = PermissionEnforcementVisitor(context)
            node.accept(enforcementVisitor)
            val enforcedPermissions = enforcementVisitor.enforcedPermissions

            if (declaredPermissions.isEmpty() && enforcedPermissions.isEmpty()) {
                return
            }

            val tooNarrow = !declaredPermissions.covers(enforcedPermissions)
            val tooBroad = !enforcedPermissions.covers(declaredPermissions)

            if (tooNarrow) {
                context.report(
                    ISSUE_INCORRECT_REQUIRES_PERMISSION_PROPAGATION,
                    node,
                    context.getNameLocation(node),
                    "Method `$nodeName` is missing a @RequiresPermission annotation or it's too narrow. " +
                            "It calls APIs that require $enforcedPermissions but is only annotated with $declaredPermissions."
                )
            } else if (tooBroad) {
                context.report(
                    ISSUE_INCORRECT_REQUIRES_PERMISSION_PROPAGATION,
                    node,
                    context.getNameLocation(node),
                    "Method `$nodeName` has a broader @RequiresPermission annotation than necessary. " +
                            "It is annotated with $declaredPermissions but only calls APIs requiring $enforcedPermissions."
                )
            }
        }
    }

    private inner class PermissionEnforcementVisitor(
        private val context: JavaContext
    ) : AbstractUastVisitor() {
        val enforcedPermissions = PermissionHolder()
        private var isIdentityCleared = false

        override fun visitCallExpression(node: UCallExpression): Boolean {
            val method = node.resolve() ?: return true

            val qualifiedName = method.containingClass?.qualifiedName
            if (qualifiedName == "android.os.Binder") {
                when (method.name) {
                    "clearCallingIdentity" -> {
                        isIdentityCleared = true
                        return true
                    }
                    "restoreCallingIdentity" -> {
                        isIdentityCleared = false
                        return true
                    }
                }
            }

            if (isIdentityCleared) {
                return true
            }

            // Enforcement of a method annotated `@RequiresPermission` is done by `RequiresPermissionVisitor`
            context.evaluator.getAnnotation(method, ANNOTATION_REQUIRES_PERMISSION)?.let {
                enforcedPermissions.addAll(parseAnnotation(context, it))
                return true
            }

            // Enforcement of a method annotated `@EnforcePermission` is done by `EnforcePermissionDetector`
            context.evaluator.getAnnotation(method, ANNOTATION_ENFORCE_PERMISSION)?.let {
                enforcedPermissions.addAll(parseAnnotation(context, it))
                return true
            }

            checkEnforcement(node, method)

            return true
        }

        private fun checkEnforcement(node: UCallExpression, method: PsiMethod) {
            if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT, false) &&
                method.name.matches(CONTEXT_ENFORCEMENT_METHOD_REGEX)) {
                node.valueArguments.getOrNull(0)?.let { arg ->
                    ConstantEvaluator.evaluate(context, arg)?.toString()?.let {
                        enforcedPermissions.allOf.add(it)
                    }
                }
            } else if (context.evaluator.isMemberInSubClassOf(method, CLASS_PERMISSION_CHECKER, false) &&
                method.name.matches(PERMISSION_CHECKER_ENFORCEMENT_METHOD_REGEX)) {
                node.valueArguments.getOrNull(1)?.let { arg ->
                    ConstantEvaluator.evaluate(context, arg)?.toString()?.let {
                        enforcedPermissions.allOf.add(it)
                    }
                }
            }
        }
    }

    private fun getRequiredPermissionsFromSuper(context: JavaContext, method: UMethod): PermissionHolder {
        val holder = PermissionHolder()
        method.javaPsi.findSuperMethods().forEach { superMethod ->
            context.evaluator.getAnnotation(superMethod, ANNOTATION_REQUIRES_PERMISSION)
                ?.let { holder.addAll(parseAnnotation(context, it)) }
        }
        return holder
    }

    private fun getRequiredPermissionsFromMethod(context: JavaContext, method: UMethod): PermissionHolder {
        return context.evaluator.getAnnotation(method, ANNOTATION_REQUIRES_PERMISSION)
            ?.let { parseAnnotation(context, it) }
            ?: PermissionHolder()
    }

    private fun parseAnnotation(context: JavaContext, annotation: UAnnotation): PermissionHolder {
        val holder = PermissionHolder()

        fun getPermissions(value: UExpression?, context: JavaContext): Set<String> {
            if (value == null) return emptySet()

            if (value is UCallExpression && value.kind.name == "array_initializer") {
                return value.valueArguments
                    .mapNotNull { arg -> ConstantEvaluator.evaluate(context, arg)?.toString() }
                    .filter { it.isNotEmpty() }
                    .toSet()
            }

            val evaluated = ConstantEvaluator.evaluate(context, value)
            return if (evaluated is String && evaluated.isNotEmpty()) {
                setOf(evaluated)
            } else {
                emptySet()
            }
        }

        val valuePerms = getPermissions(annotation.findAttributeValue("value"), context)
        val allOfPerms = getPermissions(annotation.findAttributeValue("allOf"), context)
        val anyOfPerms = getPermissions(annotation.findAttributeValue("anyOf"), context)

        holder.allOf.addAll(valuePerms)
        holder.allOf.addAll(allOfPerms)
        holder.anyOf.addAll(anyOfPerms)

        return holder
    }

    private data class PermissionHolder(
        val allOf: MutableSet<String> = mutableSetOf(),
        val anyOf: MutableSet<String> = mutableSetOf(),
    ) {
        fun isEmpty() = allOf.isEmpty() && anyOf.isEmpty()

        fun addAll(other: PermissionHolder) {
            allOf.addAll(other.allOf)
            anyOf.addAll(other.anyOf)
        }

        fun covers(other: PermissionHolder): Boolean {
            val allMet = allOf.containsAll(other.allOf)

            val anyMet = if (other.anyOf.isEmpty()) {
                true
            } else {
                other.anyOf.any { it in allOf || it in anyOf }
            }
            return allMet && anyMet
        }

        override fun toString(): String {
            if (isEmpty()) return "[none]"
            val parts = mutableListOf<String>()
            if (allOf.isNotEmpty()) parts.add("allOf=$allOf")
            if (anyOf.isNotEmpty()) parts.add("anyOf=$anyOf")
            return parts.joinToString(separator = ", ", prefix = "{", postfix = "}")
        }
    }

    companion object {
        private val CONTEXT_ENFORCEMENT_METHOD_REGEX =
            "^(enforce|check)(Calling)?(OrSelf)?Permission$".toRegex()
        private val PERMISSION_CHECKER_ENFORCEMENT_METHOD_REGEX =
            "^check.*Permission$".toRegex()

        @JvmField
        val ISSUE_MISSING_OR_MISMATCHED_REQUIRES_PERMISSION_ANNOTATION = Issue.create(
            id = "MissingOrMismatchedRequiresPermissionAnnotation",
            briefDescription = "Missing or mismatched @RequiresPermission on implementation.",
            explanation = """
                An overriding method must be annotated with @RequiresPermission and it must be
                equivalent to the annotation on the super method.",
            """.trimIndent(),
            category = Category.SECURITY,
            priority = 6,
            severity = Severity.ERROR,
            implementation = Implementation(
                RequiresPermissionDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )

        @JvmField
        val ISSUE_INCORRECT_REQUIRES_PERMISSION_PROPAGATION = Issue.create(
            id = "IncorrectRequiresPermissionPropagation",
            briefDescription = "Incorrectly propagating @RequiresPermission",
            explanation = """
                Methods that call other APIs requiring permissions must be annotated with their own
                @RequiresPermission annotation.
                This annotation must be specific enough to cover all permissions required by the
                APIs it calls (not "too narrow"), but should not declare permissions that are
                never used (not "too broad").
            """.trimIndent(),
            category = Category.SECURITY,
            priority = 6,
            severity = Severity.ERROR,
            implementation = Implementation(
                RequiresPermissionDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}
+665 −0

File added.

Preview size limit exceeded, changes collapsed.