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 decltype(auto) callsName(const char *FunctionName) {
56   return callee(functionDecl(hasName(FunctionName)));
57 }
58 
59 decltype(auto) equalsBoundArgDecl(int ArgIdx, const char *DeclName) {
60   return hasArgument(ArgIdx, ignoringParenCasts(declRefExpr(
61                                  to(varDecl(equalsBoundNode(DeclName))))));
62 }
63 
64 decltype(auto) bindAssignmentToDecl(const char *DeclName) {
65   return hasLHS(ignoringParenImpCasts(
66                          declRefExpr(to(varDecl().bind(DeclName)))));
67 }
68 
69 /// The pattern is very common in tests, and it is OK to use it there.
70 /// We have to heuristics for detecting tests: method name starts with "test"
71 /// (used in XCTest), and a class name contains "mock" or "test" (used in
72 /// helpers which are not tests themselves, but used exclusively in tests).
73 static bool isTest(const Decl *D) {
74   if (const auto* ND = dyn_cast<NamedDecl>(D)) {
75     std::string DeclName = ND->getNameAsString();
76     if (StringRef(DeclName).startswith("test"))
77       return true;
78   }
79   if (const auto *OD = dyn_cast<ObjCMethodDecl>(D)) {
80     if (const auto *CD = dyn_cast<ObjCContainerDecl>(OD->getParent())) {
81       std::string ContainerName = CD->getNameAsString();
82       StringRef CN(ContainerName);
83       if (CN.contains_lower("test") || CN.contains_lower("mock"))
84         return true;
85     }
86   }
87   return false;
88 }
89 
90 static auto findGCDAntiPatternWithSemaphore() -> decltype(compoundStmt()) {
91 
92   const char *SemaphoreBinding = "semaphore_name";
93   auto SemaphoreCreateM = callExpr(allOf(
94       callsName("dispatch_semaphore_create"),
95       hasArgument(0, ignoringParenCasts(integerLiteral(equals(0))))));
96 
97   auto SemaphoreBindingM = anyOf(
98       forEachDescendant(
99           varDecl(hasDescendant(SemaphoreCreateM)).bind(SemaphoreBinding)),
100       forEachDescendant(binaryOperator(bindAssignmentToDecl(SemaphoreBinding),
101                      hasRHS(SemaphoreCreateM))));
102 
103   auto HasBlockArgumentM = hasAnyArgument(hasType(
104             hasCanonicalType(blockPointerType())
105             ));
106 
107   auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(
108           allOf(
109               callsName("dispatch_semaphore_signal"),
110               equalsBoundArgDecl(0, SemaphoreBinding)
111               )))));
112 
113   auto HasBlockAndCallsSignalM = allOf(HasBlockArgumentM, ArgCallsSignalM);
114 
115   auto HasBlockCallingSignalM =
116     forEachDescendant(
117       stmt(anyOf(
118         callExpr(HasBlockAndCallsSignalM),
119         objcMessageExpr(HasBlockAndCallsSignalM)
120            )));
121 
122   auto SemaphoreWaitM = forEachDescendant(
123     callExpr(
124       allOf(
125         callsName("dispatch_semaphore_wait"),
126         equalsBoundArgDecl(0, SemaphoreBinding)
127       )
128     ).bind(WarnAtNode));
129 
130   return compoundStmt(
131       SemaphoreBindingM, HasBlockCallingSignalM, SemaphoreWaitM);
132 }
133 
134 static auto findGCDAntiPatternWithGroup() -> decltype(compoundStmt()) {
135 
136   const char *GroupBinding = "group_name";
137   auto DispatchGroupCreateM = callExpr(callsName("dispatch_group_create"));
138 
139   auto GroupBindingM = anyOf(
140       forEachDescendant(
141           varDecl(hasDescendant(DispatchGroupCreateM)).bind(GroupBinding)),
142       forEachDescendant(binaryOperator(bindAssignmentToDecl(GroupBinding),
143                      hasRHS(DispatchGroupCreateM))));
144 
145   auto GroupEnterM = forEachDescendant(
146       stmt(callExpr(allOf(callsName("dispatch_group_enter"),
147                           equalsBoundArgDecl(0, GroupBinding)))));
148 
149   auto HasBlockArgumentM = hasAnyArgument(hasType(
150             hasCanonicalType(blockPointerType())
151             ));
152 
153   auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(
154           allOf(
155               callsName("dispatch_group_leave"),
156               equalsBoundArgDecl(0, GroupBinding)
157               )))));
158 
159   auto HasBlockAndCallsLeaveM = allOf(HasBlockArgumentM, ArgCallsSignalM);
160 
161   auto AcceptsBlockM =
162     forEachDescendant(
163       stmt(anyOf(
164         callExpr(HasBlockAndCallsLeaveM),
165         objcMessageExpr(HasBlockAndCallsLeaveM)
166            )));
167 
168   auto GroupWaitM = forEachDescendant(
169     callExpr(
170       allOf(
171         callsName("dispatch_group_wait"),
172         equalsBoundArgDecl(0, GroupBinding)
173       )
174     ).bind(WarnAtNode));
175 
176   return compoundStmt(GroupBindingM, GroupEnterM, AcceptsBlockM, GroupWaitM);
177 }
178 
179 static void emitDiagnostics(const BoundNodes &Nodes,
180                             const char* Type,
181                             BugReporter &BR,
182                             AnalysisDeclContext *ADC,
183                             const GCDAntipatternChecker *Checker) {
184   const auto *SW = Nodes.getNodeAs<CallExpr>(WarnAtNode);
185   assert(SW);
186 
187   std::string Diagnostics;
188   llvm::raw_string_ostream OS(Diagnostics);
189   OS << "Waiting on a callback using a " << Type << " creates useless threads "
190      << "and is subject to priority inversion; consider "
191      << "using a synchronous API or changing the caller to be asynchronous";
192 
193   BR.EmitBasicReport(
194     ADC->getDecl(),
195     Checker,
196     /*Name=*/"GCD performance anti-pattern",
197     /*BugCategory=*/"Performance",
198     OS.str(),
199     PathDiagnosticLocation::createBegin(SW, BR.getSourceManager(), ADC),
200     SW->getSourceRange());
201 }
202 
203 void GCDAntipatternChecker::checkASTCodeBody(const Decl *D,
204                                              AnalysisManager &AM,
205                                              BugReporter &BR) const {
206   if (isTest(D))
207     return;
208 
209   AnalysisDeclContext *ADC = AM.getAnalysisDeclContext(D);
210 
211   auto SemaphoreMatcherM = findGCDAntiPatternWithSemaphore();
212   auto Matches = match(SemaphoreMatcherM, *D->getBody(), AM.getASTContext());
213   for (BoundNodes Match : Matches)
214     emitDiagnostics(Match, "semaphore", BR, ADC, this);
215 
216   auto GroupMatcherM = findGCDAntiPatternWithGroup();
217   Matches = match(GroupMatcherM, *D->getBody(), AM.getASTContext());
218   for (BoundNodes Match : Matches)
219     emitDiagnostics(Match, "group", BR, ADC, this);
220 }
221 
222 } // end of anonymous namespace
223 
224 void ento::registerGCDAntipattern(CheckerManager &Mgr) {
225   Mgr.registerChecker<GCDAntipatternChecker>();
226 }
227 
228 bool ento::shouldRegisterGCDAntipattern(const CheckerManager &mgr) {
229   return true;
230 }
231