1//=- RunLoopAutoreleaseLeakChecker.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//
10// A checker for detecting leaks resulting from allocating temporary
11// autoreleased objects before starting the main run loop.
12//
13// Checks for two antipatterns:
14// 1. ObjCMessageExpr followed by [[NSRunLoop mainRunLoop] run] in the same
15// autorelease pool.
16// 2. ObjCMessageExpr followed by [[NSRunLoop mainRunLoop] run] in no
17// autorelease pool.
18//
19// Any temporary objects autoreleased in code called in those expressions
20// will not be deallocated until the program exits, and are effectively leaks.
21//
22//===----------------------------------------------------------------------===//
23//
24
25#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
26#include "clang/AST/Decl.h"
27#include "clang/AST/DeclObjC.h"
28#include "clang/ASTMatchers/ASTMatchFinder.h"
29#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
30#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
31#include "clang/StaticAnalyzer/Core/Checker.h"
32#include "clang/StaticAnalyzer/Core/CheckerManager.h"
33#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
34#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
35#include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h"
36
37using namespace clang;
38using namespace ento;
39using namespace ast_matchers;
40
41namespace {
42
43const char * RunLoopBind = "NSRunLoopM";
44const char * RunLoopRunBind = "RunLoopRunM";
45const char * OtherMsgBind = "OtherMessageSentM";
46const char * AutoreleasePoolBind = "AutoreleasePoolM";
47const char * OtherStmtAutoreleasePoolBind = "OtherAutoreleasePoolM";
48
49class RunLoopAutoreleaseLeakChecker : public Checker<check::ASTCodeBody> {
50
51public:
52  void checkASTCodeBody(const Decl *D,
53                        AnalysisManager &AM,
54                        BugReporter &BR) const;
55
56};
57
58} // end anonymous namespace
59
60/// \return Whether @c A occurs before @c B in traversal of
61/// @c Parent.
62/// Conceptually a very incomplete/unsound approximation of happens-before
63/// relationship (A is likely to be evaluated before B),
64/// but useful enough in this case.
65static bool seenBefore(const Stmt *Parent, const Stmt *A, const Stmt *B) {
66  for (const Stmt *C : Parent->children()) {
67    if (!C) continue;
68
69    if (C == A)
70      return true;
71
72    if (C == B)
73      return false;
74
75    return seenBefore(C, A, B);
76  }
77  return false;
78}
79
80static void emitDiagnostics(BoundNodes &Match,
81                            const Decl *D,
82                            BugReporter &BR,
83                            AnalysisManager &AM,
84                            const RunLoopAutoreleaseLeakChecker *Checker) {
85
86  assert(D->hasBody());
87  const Stmt *DeclBody = D->getBody();
88
89  AnalysisDeclContext *ADC = AM.getAnalysisDeclContext(D);
90
91  const auto *ME = Match.getNodeAs<ObjCMessageExpr>(OtherMsgBind);
92  assert(ME);
93
94  const auto *AP =
95      Match.getNodeAs<ObjCAutoreleasePoolStmt>(AutoreleasePoolBind);
96  const auto *OAP =
97      Match.getNodeAs<ObjCAutoreleasePoolStmt>(OtherStmtAutoreleasePoolBind);
98  bool HasAutoreleasePool = (AP != nullptr);
99
100  const auto *RL = Match.getNodeAs<ObjCMessageExpr>(RunLoopBind);
101  const auto *RLR = Match.getNodeAs<Stmt>(RunLoopRunBind);
102  assert(RLR && "Run loop launch not found");
103  assert(ME != RLR);
104
105  // Launch of run loop occurs before the message-sent expression is seen.
106  if (seenBefore(DeclBody, RLR, ME))
107    return;
108
109  if (HasAutoreleasePool && (OAP != AP))
110    return;
111
112  PathDiagnosticLocation Location = PathDiagnosticLocation::createBegin(
113    ME, BR.getSourceManager(), ADC);
114  SourceRange Range = ME->getSourceRange();
115
116  BR.EmitBasicReport(ADC->getDecl(), Checker,
117                     /*Name=*/"Memory leak inside autorelease pool",
118                     /*BugCategory=*/"Memory",
119                     /*Name=*/
120                     (Twine("Temporary objects allocated in the") +
121                      " autorelease pool " +
122                      (HasAutoreleasePool ? "" : "of last resort ") +
123                      "followed by the launch of " +
124                      (RL ? "main run loop " : "xpc_main ") +
125                      "may never get released; consider moving them to a "
126                      "separate autorelease pool")
127                         .str(),
128                     Location, Range);
129}
130
131static StatementMatcher getRunLoopRunM(StatementMatcher Extra = anything()) {
132  StatementMatcher MainRunLoopM =
133      objcMessageExpr(hasSelector("mainRunLoop"),
134                      hasReceiverType(asString("NSRunLoop")),
135                      Extra)
136          .bind(RunLoopBind);
137
138  StatementMatcher MainRunLoopRunM = objcMessageExpr(hasSelector("run"),
139                         hasReceiver(MainRunLoopM),
140                         Extra).bind(RunLoopRunBind);
141
142  StatementMatcher XPCRunM =
143      callExpr(callee(functionDecl(hasName("xpc_main")))).bind(RunLoopRunBind);
144  return anyOf(MainRunLoopRunM, XPCRunM);
145}
146
147static StatementMatcher getOtherMessageSentM(StatementMatcher Extra = anything()) {
148  return objcMessageExpr(unless(anyOf(equalsBoundNode(RunLoopBind),
149                                      equalsBoundNode(RunLoopRunBind))),
150                         Extra)
151      .bind(OtherMsgBind);
152}
153
154static void
155checkTempObjectsInSamePool(const Decl *D, AnalysisManager &AM, BugReporter &BR,
156                           const RunLoopAutoreleaseLeakChecker *Chkr) {
157  StatementMatcher RunLoopRunM = getRunLoopRunM();
158  StatementMatcher OtherMessageSentM = getOtherMessageSentM(
159    hasAncestor(autoreleasePoolStmt().bind(OtherStmtAutoreleasePoolBind)));
160
161  StatementMatcher RunLoopInAutorelease =
162      autoreleasePoolStmt(
163        hasDescendant(RunLoopRunM),
164        hasDescendant(OtherMessageSentM)).bind(AutoreleasePoolBind);
165
166  DeclarationMatcher GroupM = decl(hasDescendant(RunLoopInAutorelease));
167
168  auto Matches = match(GroupM, *D, AM.getASTContext());
169  for (BoundNodes Match : Matches)
170    emitDiagnostics(Match, D, BR, AM, Chkr);
171}
172
173static void
174checkTempObjectsInNoPool(const Decl *D, AnalysisManager &AM, BugReporter &BR,
175                         const RunLoopAutoreleaseLeakChecker *Chkr) {
176
177  auto NoPoolM = unless(hasAncestor(autoreleasePoolStmt()));
178
179  StatementMatcher RunLoopRunM = getRunLoopRunM(NoPoolM);
180  StatementMatcher OtherMessageSentM = getOtherMessageSentM(NoPoolM);
181
182  DeclarationMatcher GroupM = functionDecl(
183    isMain(),
184    hasDescendant(RunLoopRunM),
185    hasDescendant(OtherMessageSentM)
186  );
187
188  auto Matches = match(GroupM, *D, AM.getASTContext());
189
190  for (BoundNodes Match : Matches)
191    emitDiagnostics(Match, D, BR, AM, Chkr);
192
193}
194
195void RunLoopAutoreleaseLeakChecker::checkASTCodeBody(const Decl *D,
196                        AnalysisManager &AM,
197                        BugReporter &BR) const {
198  checkTempObjectsInSamePool(D, AM, BR, this);
199  checkTempObjectsInNoPool(D, AM, BR, this);
200}
201
202void ento::registerRunLoopAutoreleaseLeakChecker(CheckerManager &mgr) {
203  mgr.registerChecker<RunLoopAutoreleaseLeakChecker>();
204}
205
206bool ento::shouldRegisterRunLoopAutoreleaseLeakChecker(const CheckerManager &mgr) {
207  return true;
208}
209