1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.lint.aidl 18 19 import com.android.tools.lint.client.api.UElementHandler 20 import com.android.tools.lint.detector.api.AnnotationInfo 21 import com.android.tools.lint.detector.api.AnnotationOrigin 22 import com.android.tools.lint.detector.api.AnnotationUsageInfo 23 import com.android.tools.lint.detector.api.AnnotationUsageType 24 import com.android.tools.lint.detector.api.Category 25 import com.android.tools.lint.detector.api.ConstantEvaluator 26 import com.android.tools.lint.detector.api.Detector 27 import com.android.tools.lint.detector.api.Implementation 28 import com.android.tools.lint.detector.api.Issue 29 import com.android.tools.lint.detector.api.JavaContext 30 import com.android.tools.lint.detector.api.Scope 31 import com.android.tools.lint.detector.api.Severity 32 import com.android.tools.lint.detector.api.SourceCodeScanner 33 import com.google.android.lint.findCallExpression 34 import com.intellij.psi.PsiAnnotation 35 import com.intellij.psi.PsiArrayInitializerMemberValue 36 import com.intellij.psi.PsiClass 37 import com.intellij.psi.PsiElement 38 import com.intellij.psi.PsiMethod 39 import org.jetbrains.uast.UBlockExpression 40 import org.jetbrains.uast.UDeclarationsExpression 41 import org.jetbrains.uast.UElement 42 import org.jetbrains.uast.UExpression 43 import org.jetbrains.uast.UMethod 44 import org.jetbrains.uast.skipParenthesizedExprDown 45 46 import java.util.EnumSet 47 48 /** 49 * Lint Detector that ensures consistency when using the @EnforcePermission 50 * annotation. Multiple verifications are implemented: 51 * 52 * 1. Visit any annotation usage, to ensure that any derived class will have 53 * the correct annotation on each methods. Even if the subclass does not 54 * have the annotation, visitAnnotationUsage will be called which allows us 55 * to capture the issue. 56 * 2. Visit any method, to ensure that if a method is annotated, it has 57 * its ancestor also annotated. This is to avoid having an annotation on a 58 * Java method without the corresponding annotation on the AIDL interface. 59 * 3. When annotated, ensures that the first instruction is to call the helper 60 * method (or the parent helper). 61 */ 62 class EnforcePermissionDetector : Detector(), SourceCodeScanner { 63 applicableAnnotationsnull64 override fun applicableAnnotations(): List<String> { 65 return listOf(ANNOTATION_ENFORCE_PERMISSION) 66 } 67 getApplicableUastTypesnull68 override fun getApplicableUastTypes(): List<Class<out UElement?>> = 69 listOf(UMethod::class.java) 70 71 private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> { 72 if (elem is PsiArrayInitializerMemberValue) 73 return elem.getInitializers().map { it as PsiElement }.toTypedArray() 74 return elem.getChildren() 75 } 76 areAnnotationsEquivalentnull77 private fun areAnnotationsEquivalent( 78 context: JavaContext, 79 anno1: PsiAnnotation, 80 anno2: PsiAnnotation 81 ): Boolean { 82 if (anno1.qualifiedName != anno2.qualifiedName) { 83 return false 84 } 85 val attr1 = anno1.parameterList.attributes 86 val attr2 = anno2.parameterList.attributes 87 if (attr1.size != attr2.size) { 88 return false 89 } 90 for (i in attr1.indices) { 91 if (attr1[i].name != attr2[i].name) { 92 return false 93 } 94 val value1 = attr1[i].value ?: return false 95 val value2 = attr2[i].value ?: return false 96 // Try to compare values directly with each other. 97 val v1 = ConstantEvaluator.evaluate(context, value1) 98 val v2 = ConstantEvaluator.evaluate(context, value2) 99 if (v1 != null && v2 != null) { 100 if (v1 != v2 && !isOneShortPermissionOfOther(v1, v2)) { 101 return false 102 } 103 } else { 104 val children1 = annotationValueGetChildren(value1) 105 val children2 = annotationValueGetChildren(value2) 106 if (children1.size != children2.size) { 107 return false 108 } 109 for (j in children1.indices) { 110 val c1 = ConstantEvaluator.evaluate(context, children1[j]) 111 val c2 = ConstantEvaluator.evaluate(context, children2[j]) 112 if (c1 != c2 && !isOneShortPermissionOfOther(c1, c2)) { 113 return false 114 } 115 } 116 } 117 } 118 return true 119 } 120 isOneShortPermissionOfOthernull121 private fun isOneShortPermissionOfOther( 122 permission1: Any?, 123 permission2: Any? 124 ): Boolean = permission1 == (permission2 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) || 125 permission2 == (permission1 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) 126 127 private fun compareMethods( 128 context: JavaContext, 129 element: UElement, 130 overridingMethod: PsiMethod, 131 overriddenMethod: PsiMethod, 132 checkEquivalence: Boolean = true 133 ) { 134 val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) 135 val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) 136 val location = context.getLocation(element) 137 val overridingClass = overridingMethod.parent as PsiClass 138 val overriddenClass = overriddenMethod.parent as PsiClass 139 val overridingName = "${overridingClass.name}.${overridingMethod.name}" 140 val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" 141 if (overridingAnnotation == null) { 142 val msg = "The method $overridingName overrides the method $overriddenName which " + 143 "is annotated with @EnforcePermission. The same annotation must be used " + 144 "on $overridingName" 145 context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) 146 } else if (overriddenAnnotation == null) { 147 val msg = "The method $overridingName overrides the method $overriddenName which " + 148 "is not annotated with @EnforcePermission. The same annotation must be " + 149 "used on $overriddenName. Did you forget to annotate the AIDL definition?" 150 context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) 151 } else if (checkEquivalence && !areAnnotationsEquivalent( 152 context, overridingAnnotation, overriddenAnnotation)) { 153 val msg = "The method $overridingName is annotated with " + 154 "${overridingAnnotation.text} which differs from the overridden " + 155 "method $overriddenName: ${overriddenAnnotation.text}. The same " + 156 "annotation must be used for both methods." 157 context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) 158 } 159 } 160 visitAnnotationUsagenull161 override fun visitAnnotationUsage( 162 context: JavaContext, 163 element: UElement, 164 annotationInfo: AnnotationInfo, 165 usageInfo: AnnotationUsageInfo 166 ) { 167 if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && 168 annotationInfo.origin == AnnotationOrigin.METHOD) { 169 /* Ignore implementations that are not a sub-class of Stub (i.e., Proxy). */ 170 val uMethod = element as? UMethod ?: return 171 if (getContainingAidlInterface(context, uMethod) == null) { 172 return 173 } 174 val overridingMethod = element.sourcePsi as PsiMethod 175 val overriddenMethod = usageInfo.referenced as PsiMethod 176 compareMethods(context, element, overridingMethod, overriddenMethod) 177 } 178 } 179 createUastHandlernull180 override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) 181 182 private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { 183 override fun visitMethod(node: UMethod) { 184 if (context.evaluator.isAbstract(node)) return 185 if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return 186 187 val stubClass = containingStub(context, node) 188 if (stubClass == null) { 189 context.report( 190 ISSUE_MISUSING_ENFORCE_PERMISSION, 191 node, 192 context.getLocation(node), 193 "The class of ${node.name} does not inherit from an AIDL generated Stub class" 194 ) 195 return 196 } 197 198 /* Check that we are connected to the super class */ 199 val overridingMethod = node as PsiMethod 200 val parents = overridingMethod.findSuperMethods(stubClass) 201 if (parents.isEmpty()) { 202 context.report( 203 ISSUE_MISUSING_ENFORCE_PERMISSION, 204 node, 205 context.getLocation(node), 206 "The method ${node.name} does not override an AIDL generated method" 207 ) 208 return 209 } 210 for (overriddenMethod in parents) { 211 // The equivalence check can be skipped, if both methods are 212 // annotated, it will be verified by visitAnnotationUsage. 213 compareMethods(context, node, overridingMethod, 214 overriddenMethod, checkEquivalence = false) 215 } 216 217 /* Check that the helper is called as a first instruction */ 218 val targetExpression = getHelperMethodCallSourceString(node) 219 val message = 220 "Method must start with $targetExpression or super.${node.name}(), if applicable" 221 222 val firstExpression = (node.uastBody as? UBlockExpression) 223 ?.expressions?.firstOrNull() 224 225 if (firstExpression == null) { 226 context.report( 227 ISSUE_ENFORCE_PERMISSION_HELPER, 228 context.getLocation(node), 229 message, 230 ) 231 return 232 } 233 234 val firstExpressionSource = firstExpression.skipParenthesizedExprDown() 235 .asSourceString() 236 .filterNot(Char::isWhitespace) 237 238 if (firstExpressionSource != targetExpression && 239 firstExpressionSource != "super.$targetExpression") { 240 // calling super.<methodName>() is also legal 241 val directSuper = context.evaluator.getSuperMethod(node) 242 val firstCall = findCallExpression(firstExpression)?.resolve() 243 if (directSuper != null && firstCall == directSuper) return 244 245 val locationTarget = getLocationTarget(firstExpression) 246 val expressionLocation = context.getLocation(locationTarget) 247 248 context.report( 249 ISSUE_ENFORCE_PERMISSION_HELPER, 250 context.getLocation(node), 251 message, 252 getHelperMethodFix(node, expressionLocation), 253 ) 254 } 255 } 256 } 257 258 companion object { 259 260 private const val HELPER_SUFFIX = "_enforcePermission" 261 262 val EXPLANATION = """ 263 The @EnforcePermission annotation is used to delegate the verification of the caller's 264 permissions to a generated AIDL method. 265 266 In order to surface that information to platform developers, the same annotation must be 267 used on the implementation class or methods. 268 269 The @EnforcePermission annotation can only be used on methods whose class extends from 270 the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the 271 AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX. 272 273 yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can 274 either call it directly, or call it indirectly via super.yourMethodName(). 275 """ 276 277 val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( 278 id = "MissingEnforcePermissionAnnotation", 279 briefDescription = "Missing @EnforcePermission annotation on Binder method", 280 explanation = EXPLANATION, 281 category = Category.SECURITY, 282 priority = 6, 283 severity = Severity.ERROR, 284 implementation = Implementation( 285 EnforcePermissionDetector::class.java, 286 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 287 ) 288 ) 289 290 val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( 291 id = "MismatchingEnforcePermissionAnnotation", 292 briefDescription = "Incorrect @EnforcePermission annotation on Binder method", 293 explanation = EXPLANATION, 294 category = Category.SECURITY, 295 priority = 6, 296 severity = Severity.ERROR, 297 implementation = Implementation( 298 EnforcePermissionDetector::class.java, 299 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 300 ) 301 ) 302 303 val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( 304 id = "MissingEnforcePermissionHelper", 305 briefDescription = """Missing permission-enforcing method call in AIDL method 306 |annotated with @EnforcePermission""".trimMargin(), 307 explanation = EXPLANATION, 308 category = Category.SECURITY, 309 priority = 6, 310 severity = Severity.ERROR, 311 implementation = Implementation( 312 EnforcePermissionDetector::class.java, 313 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 314 ) 315 ) 316 317 val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create( 318 id = "MisusingEnforcePermissionAnnotation", 319 briefDescription = "@EnforcePermission cannot be used here", 320 explanation = EXPLANATION, 321 category = Category.SECURITY, 322 priority = 6, 323 severity = Severity.ERROR, 324 implementation = Implementation( 325 EnforcePermissionDetector::class.java, 326 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 327 ) 328 ) 329 330 /** 331 * handles an edge case with UDeclarationsExpression, where sourcePsi is null, 332 * resulting in an incorrect Location if used directly 333 */ getLocationTargetnull334 private fun getLocationTarget(firstExpression: UExpression): PsiElement? { 335 if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi 336 if (firstExpression is UDeclarationsExpression) { 337 return firstExpression.declarations.firstOrNull()?.sourcePsi 338 } 339 return null 340 } 341 } 342 } 343