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