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