• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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