1//===- ObjCAutoreleaseWriteChecker.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 ObjCAutoreleaseWriteChecker which warns against writes
10// into autoreleased out parameters which cause crashes.
11// An example of a problematic write is a write to @c error in the example
12// below:
13//
14// - (BOOL) mymethod:(NSError *__autoreleasing *)error list:(NSArray*) list {
15//     [list enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
16//       NSString *myString = obj;
17//       if ([myString isEqualToString:@"error"] && error)
18//         *error = [NSError errorWithDomain:@"MyDomain" code:-1];
19//     }];
20//     return false;
21// }
22//
23// Such code will crash on read from `*error` due to the autorelease pool
24// in `enumerateObjectsUsingBlock` implementation freeing the error object
25// on exit from the function.
26//
27//===----------------------------------------------------------------------===//
28
29#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
30#include "clang/ASTMatchers/ASTMatchFinder.h"
31#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
32#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
33#include "clang/StaticAnalyzer/Core/BugReporter/CommonBugCategories.h"
34#include "clang/StaticAnalyzer/Core/Checker.h"
35#include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h"
36#include "llvm/ADT/Twine.h"
37
38using namespace clang;
39using namespace ento;
40using namespace ast_matchers;
41
42namespace {
43
44const char *ProblematicWriteBind = "problematicwrite";
45const char *CapturedBind = "capturedbind";
46const char *ParamBind = "parambind";
47const char *IsMethodBind = "ismethodbind";
48const char *IsARPBind = "isautoreleasepoolbind";
49
50class ObjCAutoreleaseWriteChecker : public Checker<check::ASTCodeBody> {
51public:
52  void checkASTCodeBody(const Decl *D,
53                        AnalysisManager &AM,
54                        BugReporter &BR) const;
55private:
56  std::vector<std::string> SelectorsWithAutoreleasingPool = {
57      // Common to NSArray,  NSSet, NSOrderedSet
58      "enumerateObjectsUsingBlock:",
59      "enumerateObjectsWithOptions:usingBlock:",
60
61      // Common to NSArray and NSOrderedSet
62      "enumerateObjectsAtIndexes:options:usingBlock:",
63      "indexOfObjectAtIndexes:options:passingTest:",
64      "indexesOfObjectsAtIndexes:options:passingTest:",
65      "indexOfObjectPassingTest:",
66      "indexOfObjectWithOptions:passingTest:",
67      "indexesOfObjectsPassingTest:",
68      "indexesOfObjectsWithOptions:passingTest:",
69
70      // NSDictionary
71      "enumerateKeysAndObjectsUsingBlock:",
72      "enumerateKeysAndObjectsWithOptions:usingBlock:",
73      "keysOfEntriesPassingTest:",
74      "keysOfEntriesWithOptions:passingTest:",
75
76      // NSSet
77      "objectsPassingTest:",
78      "objectsWithOptions:passingTest:",
79      "enumerateIndexPathsWithOptions:usingBlock:",
80
81      // NSIndexSet
82      "enumerateIndexesWithOptions:usingBlock:",
83      "enumerateIndexesUsingBlock:",
84      "enumerateIndexesInRange:options:usingBlock:",
85      "enumerateRangesUsingBlock:",
86      "enumerateRangesWithOptions:usingBlock:",
87      "enumerateRangesInRange:options:usingBlock:",
88      "indexPassingTest:",
89      "indexesPassingTest:",
90      "indexWithOptions:passingTest:",
91      "indexesWithOptions:passingTest:",
92      "indexInRange:options:passingTest:",
93      "indexesInRange:options:passingTest:"
94  };
95
96  std::vector<std::string> FunctionsWithAutoreleasingPool = {
97      "dispatch_async", "dispatch_group_async", "dispatch_barrier_async"};
98};
99}
100
101static inline std::vector<llvm::StringRef>
102toRefs(const std::vector<std::string> &V) {
103  return std::vector<llvm::StringRef>(V.begin(), V.end());
104}
105
106static decltype(auto)
107callsNames(const std::vector<std::string> &FunctionNames) {
108  return callee(functionDecl(hasAnyName(toRefs(FunctionNames))));
109}
110
111static void emitDiagnostics(BoundNodes &Match, const Decl *D, BugReporter &BR,
112                            AnalysisManager &AM,
113                            const ObjCAutoreleaseWriteChecker *Checker) {
114  AnalysisDeclContext *ADC = AM.getAnalysisDeclContext(D);
115
116  const auto *PVD = Match.getNodeAs<ParmVarDecl>(ParamBind);
117  QualType Ty = PVD->getType();
118  if (Ty->getPointeeType().getObjCLifetime() != Qualifiers::OCL_Autoreleasing)
119    return;
120  const char *ActionMsg = "Write to";
121  const auto *MarkedStmt = Match.getNodeAs<Expr>(ProblematicWriteBind);
122  bool IsCapture = false;
123
124  // Prefer to warn on write, but if not available, warn on capture.
125  if (!MarkedStmt) {
126    MarkedStmt = Match.getNodeAs<Expr>(CapturedBind);
127    assert(MarkedStmt);
128    ActionMsg = "Capture of";
129    IsCapture = true;
130  }
131
132  SourceRange Range = MarkedStmt->getSourceRange();
133  PathDiagnosticLocation Location = PathDiagnosticLocation::createBegin(
134      MarkedStmt, BR.getSourceManager(), ADC);
135
136  bool IsMethod = Match.getNodeAs<ObjCMethodDecl>(IsMethodBind) != nullptr;
137  const char *FunctionDescription = IsMethod ? "method" : "function";
138  bool IsARP = Match.getNodeAs<ObjCAutoreleasePoolStmt>(IsARPBind) != nullptr;
139
140  llvm::SmallString<128> BugNameBuf;
141  llvm::raw_svector_ostream BugName(BugNameBuf);
142  BugName << ActionMsg
143          << " autoreleasing out parameter inside autorelease pool";
144
145  llvm::SmallString<128> BugMessageBuf;
146  llvm::raw_svector_ostream BugMessage(BugMessageBuf);
147  BugMessage << ActionMsg << " autoreleasing out parameter ";
148  if (IsCapture)
149    BugMessage << "'" + PVD->getName() + "' ";
150
151  BugMessage << "inside ";
152  if (IsARP)
153    BugMessage << "locally-scoped autorelease pool;";
154  else
155    BugMessage << "autorelease pool that may exit before "
156               << FunctionDescription << " returns;";
157
158  BugMessage << " consider writing first to a strong local variable"
159                " declared outside ";
160  if (IsARP)
161    BugMessage << "of the autorelease pool";
162  else
163    BugMessage << "of the block";
164
165  BR.EmitBasicReport(ADC->getDecl(), Checker, BugName.str(),
166                     categories::MemoryRefCount, BugMessage.str(), Location,
167                     Range);
168}
169
170void ObjCAutoreleaseWriteChecker::checkASTCodeBody(const Decl *D,
171                                                  AnalysisManager &AM,
172                                                  BugReporter &BR) const {
173
174  auto DoublePointerParamM =
175      parmVarDecl(hasType(hasCanonicalType(pointerType(
176                      pointee(hasCanonicalType(objcObjectPointerType()))))))
177          .bind(ParamBind);
178
179  auto ReferencedParamM =
180      declRefExpr(to(parmVarDecl(DoublePointerParamM))).bind(CapturedBind);
181
182  // Write into a binded object, e.g. *ParamBind = X.
183  auto WritesIntoM = binaryOperator(
184    hasLHS(unaryOperator(
185        hasOperatorName("*"),
186        hasUnaryOperand(
187          ignoringParenImpCasts(ReferencedParamM))
188    )),
189    hasOperatorName("=")
190  ).bind(ProblematicWriteBind);
191
192  auto ArgumentCaptureM = hasAnyArgument(
193    ignoringParenImpCasts(ReferencedParamM));
194  auto CapturedInParamM = stmt(anyOf(
195      callExpr(ArgumentCaptureM),
196      objcMessageExpr(ArgumentCaptureM)));
197
198  // WritesIntoM happens inside a block passed as an argument.
199  auto WritesOrCapturesInBlockM = hasAnyArgument(allOf(
200      hasType(hasCanonicalType(blockPointerType())),
201      forEachDescendant(
202        stmt(anyOf(WritesIntoM, CapturedInParamM))
203      )));
204
205  auto BlockPassedToMarkedFuncM = stmt(anyOf(
206    callExpr(allOf(
207      callsNames(FunctionsWithAutoreleasingPool), WritesOrCapturesInBlockM)),
208    objcMessageExpr(allOf(
209       hasAnySelector(toRefs(SelectorsWithAutoreleasingPool)),
210       WritesOrCapturesInBlockM))
211  ));
212
213  // WritesIntoM happens inside an explicit @autoreleasepool.
214  auto WritesOrCapturesInPoolM =
215      autoreleasePoolStmt(
216          forEachDescendant(stmt(anyOf(WritesIntoM, CapturedInParamM))))
217          .bind(IsARPBind);
218
219  auto HasParamAndWritesInMarkedFuncM =
220      allOf(hasAnyParameter(DoublePointerParamM),
221            anyOf(forEachDescendant(BlockPassedToMarkedFuncM),
222                  forEachDescendant(WritesOrCapturesInPoolM)));
223
224  auto MatcherM = decl(anyOf(
225      objcMethodDecl(HasParamAndWritesInMarkedFuncM).bind(IsMethodBind),
226      functionDecl(HasParamAndWritesInMarkedFuncM),
227      blockDecl(HasParamAndWritesInMarkedFuncM)));
228
229  auto Matches = match(MatcherM, *D, AM.getASTContext());
230  for (BoundNodes Match : Matches)
231    emitDiagnostics(Match, D, BR, AM, this);
232}
233
234void ento::registerAutoreleaseWriteChecker(CheckerManager &Mgr) {
235  Mgr.registerChecker<ObjCAutoreleaseWriteChecker>();
236}
237
238bool ento::shouldRegisterAutoreleaseWriteChecker(const CheckerManager &mgr) {
239  return true;
240}
241