1 //===- GCDAntipatternChecker.cpp ---------------------------------*- C++ -*-==//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 // This file defines GCDAntipatternChecker which checks against a common
10 // antipattern when synchronous API is emulated from asynchronous callbacks
11 // using a semaphore:
12 //
13 //   dispatch_semaphore_t sema = dispatch_semaphore_create(0);
14 //
15 //   AnyCFunctionCall(^{
16 //     // code…
17 //     dispatch_semaphore_signal(sema);
18 //   })
19 //   dispatch_semaphore_wait(sema, *)
20 //
21 // Such code is a common performance problem, due to inability of GCD to
22 // properly handle QoS when a combination of queues and semaphores is used.
23 // Good code would either use asynchronous API (when available), or perform
24 // the necessary action in asynchronous callback.
25 //
26 // Currently, the check is performed using a simple heuristical AST pattern
27 // matching.
28 //
29 //===----------------------------------------------------------------------===//
30 
31 #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
32 #include "clang/ASTMatchers/ASTMatchFinder.h"
33 #include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
34 #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
35 #include "clang/StaticAnalyzer/Core/Checker.h"
36 #include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h"
37 #include "llvm/Support/Debug.h"
38 
39 using namespace clang;
40 using namespace ento;
41 using namespace ast_matchers;
42 
43 namespace {
44 
45 // ID of a node at which the diagnostic would be emitted.
46 const char *WarnAtNode = "waitcall";
47 
48 class GCDAntipatternChecker : public Checker<check::ASTCodeBody> {
49 public:
50   void checkASTCodeBody(const Decl *D,
51                         AnalysisManager &AM,
52                         BugReporter &BR) const;
53 };
54 
55 auto callsName(const char *FunctionName)
56     -> decltype(callee(functionDecl())) {
57   return callee(functionDecl(hasName(FunctionName)));
58 }
59 
60 auto equalsBoundArgDecl(int ArgIdx, const char *DeclName)
61     -> decltype(hasArgument(0, expr())) {
62   return hasArgument(ArgIdx, ignoringParenCasts(declRefExpr(
63                                  to(varDecl(equalsBoundNode(DeclName))))));
64 }
65 
66 auto bindAssignmentToDecl(const char *DeclName) -> decltype(hasLHS(expr())) {
67   return hasLHS(ignoringParenImpCasts(
68                          declRefExpr(to(varDecl().bind(DeclName)))));
69 }
70 
71 /// The pattern is very common in tests, and it is OK to use it there.
72 /// We have to heuristics for detecting tests: method name starts with "test"
73 /// (used in XCTest), and a class name contains "mock" or "test" (used in
74 /// helpers which are not tests themselves, but used exclusively in tests).
75 static bool isTest(const Decl *D) {
76   if (const auto* ND = dyn_cast<NamedDecl>(D)) {
77     std::string DeclName = ND->getNameAsString();
78     if (StringRef(DeclName).startswith("test"))
79       return true;
80   }
81   if (const auto *OD = dyn_cast<ObjCMethodDecl>(D)) {
82     if (const auto *CD = dyn_cast<ObjCContainerDecl>(OD->getParent())) {
83       std::string ContainerName = CD->getNameAsString();
84       StringRef CN(ContainerName);
85       if (CN.contains_lower("test") || CN.contains_lower("mock"))
86         return true;
87     }
88   }
89   return false;
90 }
91 
92 static auto findGCDAntiPatternWithSemaphore() -> decltype(compoundStmt()) {
93 
94   const char *SemaphoreBinding = "semaphore_name";
95   auto SemaphoreCreateM = callExpr(allOf(
96       callsName("dispatch_semaphore_create"),
97       hasArgument(0, ignoringParenCasts(integerLiteral(equals(0))))));
98 
99   auto SemaphoreBindingM = anyOf(
100       forEachDescendant(
101           varDecl(hasDescendant(SemaphoreCreateM)).bind(SemaphoreBinding)),
102       forEachDescendant(binaryOperator(bindAssignmentToDecl(SemaphoreBinding),
103                      hasRHS(SemaphoreCreateM))));
104 
105   auto HasBlockArgumentM = hasAnyArgument(hasType(
106             hasCanonicalType(blockPointerType())
107             ));
108 
109   auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(
110           allOf(
111               callsName("dispatch_semaphore_signal"),
112               equalsBoundArgDecl(0, SemaphoreBinding)
113               )))));
114 
115   auto HasBlockAndCallsSignalM = allOf(HasBlockArgumentM, ArgCallsSignalM);
116 
117   auto HasBlockCallingSignalM =
118     forEachDescendant(
119       stmt(anyOf(
120         callExpr(HasBlockAndCallsSignalM),
121         objcMessageExpr(HasBlockAndCallsSignalM)
122            )));
123 
124   auto SemaphoreWaitM = forEachDescendant(
125     callExpr(
126       allOf(
127         callsName("dispatch_semaphore_wait"),
128         equalsBoundArgDecl(0, SemaphoreBinding)
129       )
130     ).bind(WarnAtNode));
131 
132   return compoundStmt(
133       SemaphoreBindingM, HasBlockCallingSignalM, SemaphoreWaitM);
134 }
135 
136 static auto findGCDAntiPatternWithGroup() -> decltype(compoundStmt()) {
137 
138   const char *GroupBinding = "group_name";
139   auto DispatchGroupCreateM = callExpr(callsName("dispatch_group_create"));
140 
141   auto GroupBindingM = anyOf(
142       forEachDescendant(
143           varDecl(hasDescendant(DispatchGroupCreateM)).bind(GroupBinding)),
144       forEachDescendant(binaryOperator(bindAssignmentToDecl(GroupBinding),
145                      hasRHS(DispatchGroupCreateM))));
146 
147   auto GroupEnterM = forEachDescendant(
148       stmt(callExpr(allOf(callsName("dispatch_group_enter"),
149                           equalsBoundArgDecl(0, GroupBinding)))));
150 
151   auto HasBlockArgumentM = hasAnyArgument(hasType(
152             hasCanonicalType(blockPointerType())
153             ));
154 
155   auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(
156           allOf(
157               callsName("dispatch_group_leave"),
158               equalsBoundArgDecl(0, GroupBinding)
159               )))));
160 
161   auto HasBlockAndCallsLeaveM = allOf(HasBlockArgumentM, ArgCallsSignalM);
162 
163   auto AcceptsBlockM =
164     forEachDescendant(
165       stmt(anyOf(
166         callExpr(HasBlockAndCallsLeaveM),
167         objcMessageExpr(HasBlockAndCallsLeaveM)
168            )));
169 
170   auto GroupWaitM = forEachDescendant(
171     callExpr(
172       allOf(
173         callsName("dispatch_group_wait"),
174         equalsBoundArgDecl(0, GroupBinding)
175       )
176     ).bind(WarnAtNode));
177 
178   return compoundStmt(GroupBindingM, GroupEnterM, AcceptsBlockM, GroupWaitM);
179 }
180 
181 static void emitDiagnostics(const BoundNodes &Nodes,
182                             const char* Type,
183                             BugReporter &BR,
184                             AnalysisDeclContext *ADC,
185                             const GCDAntipatternChecker *Checker) {
186   const auto *SW = Nodes.getNodeAs<CallExpr>(WarnAtNode);
187   assert(SW);
188 
189   std::string Diagnostics;
190   llvm::raw_string_ostream OS(Diagnostics);
191   OS << "Waiting on a callback using a " << Type << " creates useless threads "
192      << "and is subject to priority inversion; consider "
193      << "using a synchronous API or changing the caller to be asynchronous";
194 
195   BR.EmitBasicReport(
196     ADC->getDecl(),
197     Checker,
198     /*Name=*/"GCD performance anti-pattern",
199     /*BugCategory=*/"Performance",
200     OS.str(),
201     PathDiagnosticLocation::createBegin(SW, BR.getSourceManager(), ADC),
202     SW->getSourceRange());
203 }
204 
205 void GCDAntipatternChecker::checkASTCodeBody(const Decl *D,
206                                              AnalysisManager &AM,
207                                              BugReporter &BR) const {
208   if (isTest(D))
209     return;
210 
211   AnalysisDeclContext *ADC = AM.getAnalysisDeclContext(D);
212 
213   auto SemaphoreMatcherM = findGCDAntiPatternWithSemaphore();
214   auto Matches = match(SemaphoreMatcherM, *D->getBody(), AM.getASTContext());
215   for (BoundNodes Match : Matches)
216     emitDiagnostics(Match, "semaphore", BR, ADC, this);
217 
218   auto GroupMatcherM = findGCDAntiPatternWithGroup();
219   Matches = match(GroupMatcherM, *D->getBody(), AM.getASTContext());
220   for (BoundNodes Match : Matches)
221     emitDiagnostics(Match, "group", BR, ADC, this);
222 }
223 
224 } // end of anonymous namespace
225 
226 void ento::registerGCDAntipattern(CheckerManager &Mgr) {
227   Mgr.registerChecker<GCDAntipatternChecker>();
228 }
229 
230 bool ento::shouldRegisterGCDAntipattern(const LangOptions &LO) {
231   return true;
232 }
233