1 /* 2 * Copyright (C) 2021 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.errorprone.bugpatterns.android; 18 19 import static com.google.errorprone.BugPattern.LinkType.NONE; 20 import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; 21 import static com.google.errorprone.matchers.Matchers.allOf; 22 import static com.google.errorprone.matchers.Matchers.anyOf; 23 import static com.google.errorprone.matchers.Matchers.enclosingClass; 24 import static com.google.errorprone.matchers.Matchers.instanceMethod; 25 import static com.google.errorprone.matchers.Matchers.isSubtypeOf; 26 import static com.google.errorprone.matchers.Matchers.methodInvocation; 27 import static com.google.errorprone.matchers.Matchers.methodIsNamed; 28 import static com.google.errorprone.matchers.Matchers.staticMethod; 29 30 import android.annotation.RequiresPermission; 31 import android.annotation.SuppressLint; 32 33 import com.google.auto.service.AutoService; 34 import com.google.common.base.Objects; 35 import com.google.errorprone.BugPattern; 36 import com.google.errorprone.VisitorState; 37 import com.google.errorprone.bugpatterns.BugChecker; 38 import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; 39 import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; 40 import com.google.errorprone.matchers.Description; 41 import com.google.errorprone.matchers.Matcher; 42 import com.google.errorprone.util.ASTHelpers; 43 import com.sun.source.tree.AssignmentTree; 44 import com.sun.source.tree.ClassTree; 45 import com.sun.source.tree.ExpressionTree; 46 import com.sun.source.tree.IdentifierTree; 47 import com.sun.source.tree.MemberSelectTree; 48 import com.sun.source.tree.MethodInvocationTree; 49 import com.sun.source.tree.MethodTree; 50 import com.sun.source.tree.NewClassTree; 51 import com.sun.source.tree.Tree; 52 import com.sun.source.tree.VariableTree; 53 import com.sun.source.util.TreeScanner; 54 import com.sun.tools.javac.code.Symbol; 55 import com.sun.tools.javac.code.Symbol.ClassSymbol; 56 import com.sun.tools.javac.code.Symbol.MethodSymbol; 57 import com.sun.tools.javac.code.Symbol.VarSymbol; 58 import com.sun.tools.javac.code.Type; 59 import com.sun.tools.javac.code.Type.ClassType; 60 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.Collections; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Optional; 67 import java.util.Set; 68 import java.util.concurrent.atomic.AtomicReference; 69 import java.util.function.Predicate; 70 import java.util.regex.Pattern; 71 72 import javax.lang.model.element.Name; 73 74 /** 75 * Inspects both the client and server side of AIDL interfaces to ensure that 76 * any {@code RequiresPermission} annotations are consistently declared and 77 * enforced. 78 */ 79 @AutoService(BugChecker.class) 80 @BugPattern( 81 name = "AndroidFrameworkRequiresPermission", 82 summary = "Verifies that @RequiresPermission annotations are consistent across AIDL", 83 linkType = NONE, 84 severity = WARNING) 85 public final class RequiresPermissionChecker extends BugChecker 86 implements MethodTreeMatcher, MethodInvocationTreeMatcher { 87 private static final Matcher<ExpressionTree> ENFORCE_VIA_CONTEXT = methodInvocation( 88 instanceMethod() 89 .onDescendantOf("android.content.Context") 90 .withNameMatching( 91 Pattern.compile("^(enforce|check)(Calling)?(OrSelf)?Permission$"))); 92 private static final Matcher<ExpressionTree> ENFORCE_VIA_CHECKER = methodInvocation( 93 staticMethod() 94 .onClass("android.content.PermissionChecker") 95 .withNameMatching(Pattern.compile("^check.*"))); 96 97 private static final Matcher<MethodTree> BINDER_INTERNALS = allOf( 98 enclosingClass(isSubtypeOf("android.os.IInterface")), 99 anyOf( 100 methodIsNamed("onTransact"), 101 methodIsNamed("dump"), 102 enclosingClass(simpleNameMatches(Pattern.compile("^(Stub|Default|Proxy)$"))))); 103 private static final Matcher<MethodTree> LOCAL_INTERNALS = anyOf( 104 methodIsNamed("finalize"), 105 allOf( 106 enclosingClass(isSubtypeOf("android.content.BroadcastReceiver")), 107 methodIsNamed("onReceive")), 108 allOf( 109 enclosingClass(isSubtypeOf("android.database.ContentObserver")), 110 methodIsNamed("onChange")), 111 allOf( 112 enclosingClass(isSubtypeOf("android.os.Handler")), 113 methodIsNamed("handleMessage")), 114 allOf( 115 enclosingClass(isSubtypeOf("android.os.IBinder.DeathRecipient")), 116 methodIsNamed("binderDied"))); 117 118 private static final Matcher<ExpressionTree> CLEAR_CALL = methodInvocation(staticMethod() 119 .onClass("android.os.Binder").withSignature("clearCallingIdentity()")); 120 private static final Matcher<ExpressionTree> RESTORE_CALL = methodInvocation(staticMethod() 121 .onClass("android.os.Binder").withSignature("restoreCallingIdentity(long)")); 122 123 private static final Matcher<ExpressionTree> SEND_BROADCAST = methodInvocation( 124 instanceMethod() 125 .onDescendantOf("android.content.Context") 126 .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$"))); 127 private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation( 128 instanceMethod() 129 .onDescendantOf("android.app.PendingIntent") 130 .named("send")); 131 132 private static final Matcher<ExpressionTree> INTENT_SET_ACTION = methodInvocation( 133 instanceMethod().onDescendantOf("android.content.Intent").named("setAction")); 134 135 @Override matchMethod(MethodTree tree, VisitorState state)136 public Description matchMethod(MethodTree tree, VisitorState state) { 137 // Ignore methods without an implementation 138 if (tree.getBody() == null) return Description.NO_MATCH; 139 140 // Ignore certain types of Binder generated code 141 if (BINDER_INTERNALS.matches(tree, state)) return Description.NO_MATCH; 142 143 // Ignore known-local methods which don't need to propagate 144 if (LOCAL_INTERNALS.matches(tree, state)) return Description.NO_MATCH; 145 146 // Ignore when suppressed via superclass 147 final MethodSymbol method = ASTHelpers.getSymbol(tree); 148 if (isSuppressedRecursively(method, state)) return Description.NO_MATCH; 149 150 // First, look at all outgoing method invocations to ensure that we 151 // carry those annotations forward; yell if we're too narrow 152 final ParsedRequiresPermission expectedPerm = parseRequiresPermissionRecursively( 153 method, state); 154 final ParsedRequiresPermission actualPerm = new ParsedRequiresPermission(); 155 final Description desc = tree.accept(new TreeScanner<Description, Void>() { 156 private boolean clearedCallingIdentity = false; 157 158 @Override 159 public Description visitMethodInvocation(MethodInvocationTree node, Void param) { 160 if (CLEAR_CALL.matches(node, state)) { 161 clearedCallingIdentity = true; 162 } else if (RESTORE_CALL.matches(node, state)) { 163 clearedCallingIdentity = false; 164 } else if (!clearedCallingIdentity) { 165 final ParsedRequiresPermission nodePerm = parseRequiresPermissionRecursively( 166 node, state); 167 if (!expectedPerm.containsAll(nodePerm)) { 168 return buildDescription(node) 169 .setMessage("Method " + method.name.toString() + "() annotated " 170 + expectedPerm 171 + " but too narrow; invokes method requiring " + nodePerm) 172 .build(); 173 } else { 174 actualPerm.addAll(nodePerm); 175 } 176 } 177 return super.visitMethodInvocation(node, param); 178 } 179 180 @Override 181 public Description reduce(Description r1, Description r2) { 182 return (r1 != null) ? r1 : r2; 183 } 184 }, null); 185 if (desc != null) return desc; 186 187 // Second, determine if we actually used all permissions that we claim 188 // to require; yell if we're too broad 189 if (!actualPerm.containsAll(expectedPerm)) { 190 return buildDescription(tree) 191 .setMessage("Method " + method.name.toString() + "() annotated " + expectedPerm 192 + " but too wide; only invokes methods requiring " + actualPerm 193 + "\n If calling an AIDL interface, it can be annotated by adding:" 194 + "\n @JavaPassthrough(annotation=\"" 195 + "@android.annotation.RequiresPermission(...)\")") 196 .build(); 197 } 198 199 return Description.NO_MATCH; 200 } 201 202 @Override matchMethodInvocation(MethodInvocationTree tree, VisitorState state)203 public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { 204 if (SEND_BROADCAST.matches(tree, state)) { 205 final ParsedRequiresPermission sourcePerm = 206 parseBroadcastSourceRequiresPermission(tree, state); 207 final ParsedRequiresPermission targetPerm = 208 parseBroadcastTargetRequiresPermission(tree, state); 209 if (sourcePerm == null) { 210 return buildDescription(tree) 211 .setMessage("Failed to resolve broadcast intent action for validation") 212 .build(); 213 } else if (!Objects.equal(sourcePerm, targetPerm)) { 214 return buildDescription(tree) 215 .setMessage("Broadcast annotated " + sourcePerm + " but protected with " 216 + targetPerm) 217 .build(); 218 } 219 } 220 return Description.NO_MATCH; 221 } 222 223 static class ParsedRequiresPermission { 224 final Set<String> allOf = new HashSet<>(); 225 final Set<String> anyOf = new HashSet<>(); 226 isEmpty()227 public boolean isEmpty() { 228 return allOf.isEmpty() && anyOf.isEmpty(); 229 } 230 231 /** 232 * Validate that this annotation effectively "contains" the given 233 * annotation. This is typically used to ensure that a method carries 234 * along all relevant annotations for the methods it invokes. 235 */ containsAll(ParsedRequiresPermission perm)236 public boolean containsAll(ParsedRequiresPermission perm) { 237 boolean allMet = allOf.containsAll(perm.allOf); 238 boolean anyMet = false; 239 if (perm.anyOf.isEmpty()) { 240 anyMet = true; 241 } else { 242 for (String anyPerm : perm.anyOf) { 243 if (allOf.contains(anyPerm) || anyOf.contains(anyPerm)) { 244 anyMet = true; 245 } 246 } 247 } 248 return allMet && anyMet; 249 } 250 251 @Override equals(Object obj)252 public boolean equals(Object obj) { 253 if (obj instanceof ParsedRequiresPermission) { 254 final ParsedRequiresPermission other = (ParsedRequiresPermission) obj; 255 return allOf.equals(other.allOf) && anyOf.equals(other.anyOf); 256 } else { 257 return false; 258 } 259 } 260 261 @Override toString()262 public String toString() { 263 if (isEmpty()) { 264 return "[none]"; 265 } 266 String res = "{allOf=" + allOf; 267 if (!anyOf.isEmpty()) { 268 res += " anyOf=" + anyOf; 269 } 270 res += "}"; 271 return res; 272 } 273 from(RequiresPermission perm)274 public static ParsedRequiresPermission from(RequiresPermission perm) { 275 final ParsedRequiresPermission res = new ParsedRequiresPermission(); 276 res.addAll(perm); 277 return res; 278 } 279 addAll(ParsedRequiresPermission perm)280 public void addAll(ParsedRequiresPermission perm) { 281 if (perm == null) return; 282 this.allOf.addAll(perm.allOf); 283 this.anyOf.addAll(perm.anyOf); 284 } 285 addAll(RequiresPermission perm)286 public void addAll(RequiresPermission perm) { 287 if (perm == null) return; 288 if (!perm.value().isEmpty()) this.allOf.add(perm.value()); 289 if (perm.allOf() != null) this.allOf.addAll(Arrays.asList(perm.allOf())); 290 if (perm.anyOf() != null) this.anyOf.addAll(Arrays.asList(perm.anyOf())); 291 } 292 addConstValue(Tree tree)293 public void addConstValue(Tree tree) { 294 final Object value = ASTHelpers.constValue(tree); 295 if (value != null) { 296 allOf.add(String.valueOf(value)); 297 } 298 } 299 } 300 findArgumentByParameterName(MethodInvocationTree tree, Predicate<String> paramName)301 private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree, 302 Predicate<String> paramName) { 303 final MethodSymbol sym = ASTHelpers.getSymbol(tree); 304 final List<VarSymbol> params = sym.getParameters(); 305 for (int i = 0; i < params.size(); i++) { 306 if (paramName.test(params.get(i).name.toString())) { 307 return tree.getArguments().get(i); 308 } 309 } 310 return null; 311 } 312 resolveName(ExpressionTree tree)313 private static Name resolveName(ExpressionTree tree) { 314 if (tree instanceof IdentifierTree) { 315 return ((IdentifierTree) tree).getName(); 316 } else if (tree instanceof MemberSelectTree) { 317 return resolveName(((MemberSelectTree) tree).getExpression()); 318 } else { 319 return null; 320 } 321 } 322 parseIntentAction(NewClassTree tree)323 private static ParsedRequiresPermission parseIntentAction(NewClassTree tree) { 324 final Optional<? extends ExpressionTree> arg = tree.getArguments().stream().findFirst(); 325 if (arg.isPresent()) { 326 return ParsedRequiresPermission.from( 327 ASTHelpers.getAnnotation(arg.get(), RequiresPermission.class)); 328 } else { 329 return null; 330 } 331 } 332 parseIntentAction(MethodInvocationTree tree)333 private static ParsedRequiresPermission parseIntentAction(MethodInvocationTree tree) { 334 return ParsedRequiresPermission.from(ASTHelpers.getAnnotation( 335 tree.getArguments().get(0), RequiresPermission.class)); 336 } 337 parseBroadcastSourceRequiresPermission( MethodInvocationTree methodTree, VisitorState state)338 private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission( 339 MethodInvocationTree methodTree, VisitorState state) { 340 final ExpressionTree arg = findArgumentByParameterName(methodTree, 341 (name) -> name.toLowerCase().contains("intent")); 342 if (arg instanceof IdentifierTree) { 343 final Name argName = ((IdentifierTree) arg).getName(); 344 final MethodTree method = state.findEnclosing(MethodTree.class); 345 final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>(); 346 method.accept(new TreeScanner<Void, Void>() { 347 private ParsedRequiresPermission last; 348 349 @Override 350 public Void visitMethodInvocation(MethodInvocationTree tree, Void param) { 351 if (Objects.equal(methodTree, tree)) { 352 res.set(last); 353 } else { 354 final Name name = resolveName(tree.getMethodSelect()); 355 if (Objects.equal(argName, name) 356 && INTENT_SET_ACTION.matches(tree, state)) { 357 last = parseIntentAction(tree); 358 } 359 } 360 return super.visitMethodInvocation(tree, param); 361 } 362 363 @Override 364 public Void visitAssignment(AssignmentTree tree, Void param) { 365 final Name name = resolveName(tree.getVariable()); 366 final Tree init = tree.getExpression(); 367 if (Objects.equal(argName, name) 368 && init instanceof NewClassTree) { 369 last = parseIntentAction((NewClassTree) init); 370 } 371 return super.visitAssignment(tree, param); 372 } 373 374 @Override 375 public Void visitVariable(VariableTree tree, Void param) { 376 final Name name = tree.getName(); 377 final ExpressionTree init = tree.getInitializer(); 378 if (Objects.equal(argName, name) 379 && init instanceof NewClassTree) { 380 last = parseIntentAction((NewClassTree) init); 381 } 382 return super.visitVariable(tree, param); 383 } 384 }, null); 385 return res.get(); 386 } 387 return null; 388 } 389 parseBroadcastTargetRequiresPermission( MethodInvocationTree tree, VisitorState state)390 private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission( 391 MethodInvocationTree tree, VisitorState state) { 392 final ExpressionTree arg = findArgumentByParameterName(tree, 393 (name) -> name.toLowerCase().contains("permission")); 394 final ParsedRequiresPermission res = new ParsedRequiresPermission(); 395 if (arg != null) { 396 arg.accept(new TreeScanner<Void, Void>() { 397 @Override 398 public Void visitIdentifier(IdentifierTree tree, Void param) { 399 res.addConstValue(tree); 400 return super.visitIdentifier(tree, param); 401 } 402 403 @Override 404 public Void visitMemberSelect(MemberSelectTree tree, Void param) { 405 res.addConstValue(tree); 406 return super.visitMemberSelect(tree, param); 407 } 408 }, null); 409 } 410 return res; 411 } 412 parseRequiresPermissionRecursively( MethodInvocationTree tree, VisitorState state)413 private static ParsedRequiresPermission parseRequiresPermissionRecursively( 414 MethodInvocationTree tree, VisitorState state) { 415 if (ENFORCE_VIA_CONTEXT.matches(tree, state) && tree.getArguments().size() > 0) { 416 final ParsedRequiresPermission res = new ParsedRequiresPermission(); 417 res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(0)))); 418 return res; 419 } else if (ENFORCE_VIA_CHECKER.matches(tree, state) && tree.getArguments().size() > 1) { 420 final ParsedRequiresPermission res = new ParsedRequiresPermission(); 421 res.allOf.add(String.valueOf(ASTHelpers.constValue(tree.getArguments().get(1)))); 422 return res; 423 } else { 424 final MethodSymbol method = ASTHelpers.getSymbol(tree); 425 return parseRequiresPermissionRecursively(method, state); 426 } 427 } 428 429 /** 430 * Parse any {@code RequiresPermission} annotations associated with the 431 * given method, defined either directly on the method or by any superclass. 432 */ parseRequiresPermissionRecursively( MethodSymbol method, VisitorState state)433 private static ParsedRequiresPermission parseRequiresPermissionRecursively( 434 MethodSymbol method, VisitorState state) { 435 final List<MethodSymbol> symbols = new ArrayList<>(); 436 symbols.add(method); 437 symbols.addAll(ASTHelpers.findSuperMethods(method, state.getTypes())); 438 439 final ParsedRequiresPermission res = new ParsedRequiresPermission(); 440 for (MethodSymbol symbol : symbols) { 441 res.addAll(symbol.getAnnotation(RequiresPermission.class)); 442 } 443 return res; 444 } 445 isSuppressedRecursively(MethodSymbol method, VisitorState state)446 private boolean isSuppressedRecursively(MethodSymbol method, VisitorState state) { 447 // Is method suppressed anywhere? 448 if (isSuppressed(method)) return true; 449 for (MethodSymbol symbol : ASTHelpers.findSuperMethods(method, state.getTypes())) { 450 if (isSuppressed(symbol)) return true; 451 } 452 453 // Is class suppressed anywhere? 454 final ClassSymbol clazz = ASTHelpers.enclosingClass(method); 455 if (isSuppressed(clazz)) return true; 456 Type type = clazz.getSuperclass(); 457 while (type != null) { 458 if (isSuppressed(type.tsym)) return true; 459 if (type instanceof ClassType) { 460 type = ((ClassType) type).supertype_field; 461 } else { 462 type = null; 463 } 464 } 465 return false; 466 } 467 isSuppressed(Symbol symbol)468 public boolean isSuppressed(Symbol symbol) { 469 return isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressWarnings.class)) 470 || isSuppressed(ASTHelpers.getAnnotation(symbol, SuppressLint.class)); 471 } 472 isSuppressed(SuppressWarnings anno)473 private boolean isSuppressed(SuppressWarnings anno) { 474 return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames()); 475 } 476 isSuppressed(SuppressLint anno)477 private boolean isSuppressed(SuppressLint anno) { 478 return (anno != null) && !Collections.disjoint(Arrays.asList(anno.value()), allNames()); 479 } 480 simpleNameMatches(Pattern pattern)481 static Matcher<ClassTree> simpleNameMatches(Pattern pattern) { 482 return new Matcher<ClassTree>() { 483 @Override 484 public boolean matches(ClassTree tree, VisitorState state) { 485 final CharSequence name = tree.getSimpleName().toString(); 486 return pattern.matcher(name).matches(); 487 } 488 }; 489 } 490 } 491