• 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.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