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(InnerMatcher: functionDecl(hasAnyName(toRefs(V: 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>(ID: 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>(ID: 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>(ID: CapturedBind);
127 assert(MarkedStmt);
128 ActionMsg = "Capture of";
129 IsCapture = true;
130 }
131
132 SourceRange Range = MarkedStmt->getSourceRange();
133 PathDiagnosticLocation Location = PathDiagnosticLocation::createBegin(
134 S: MarkedStmt, SM: BR.getSourceManager(), LAC: ADC);
135
136 bool IsMethod = Match.getNodeAs<ObjCMethodDecl>(ID: IsMethodBind) != nullptr;
137 const char *FunctionDescription = IsMethod ? "method" : "function";
138 bool IsARP = Match.getNodeAs<ObjCAutoreleasePoolStmt>(ID: 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(DeclWithIssue: ADC->getDecl(), Checker, BugName: BugName.str(),
166 BugCategory: categories::MemoryRefCount, BugStr: BugMessage.str(), Loc: Location,
167 Ranges: Range);
168}
169
170void ObjCAutoreleaseWriteChecker::checkASTCodeBody(const Decl *D,
171 AnalysisManager &AM,
172 BugReporter &BR) const {
173
174 auto DoublePointerParamM =
175 parmVarDecl(hasType(InnerMatcher: hasCanonicalType(InnerMatcher: pointerType(
176 pointee(hasCanonicalType(InnerMatcher: objcObjectPointerType()))))))
177 .bind(ID: ParamBind);
178
179 auto ReferencedParamM =
180 declRefExpr(to(InnerMatcher: parmVarDecl(DoublePointerParamM))).bind(ID: CapturedBind);
181
182 // Write into a binded object, e.g. *ParamBind = X.
183 auto WritesIntoM = binaryOperator(
184 hasLHS(InnerMatcher: unaryOperator(
185 hasOperatorName(Name: "*"),
186 hasUnaryOperand(
187 InnerMatcher: ignoringParenImpCasts(InnerMatcher: ReferencedParamM))
188 )),
189 hasOperatorName(Name: "=")
190 ).bind(ID: ProblematicWriteBind);
191
192 auto ArgumentCaptureM = hasAnyArgument(
193 InnerMatcher: ignoringParenImpCasts(InnerMatcher: 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(InnerMatcher: allOf(
200 hasType(InnerMatcher: hasCanonicalType(InnerMatcher: blockPointerType())),
201 forEachDescendant(
202 stmt(anyOf(WritesIntoM, CapturedInParamM))
203 )));
204
205 auto BlockPassedToMarkedFuncM = stmt(anyOf(
206 callExpr(allOf(
207 callsNames(FunctionNames: FunctionsWithAutoreleasingPool), WritesOrCapturesInBlockM)),
208 objcMessageExpr(allOf(
209 hasAnySelector(toRefs(V: 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(ID: IsARPBind);
218
219 auto HasParamAndWritesInMarkedFuncM =
220 allOf(hasAnyParameter(InnerMatcher: DoublePointerParamM),
221 anyOf(forEachDescendant(BlockPassedToMarkedFuncM),
222 forEachDescendant(WritesOrCapturesInPoolM)));
223
224 auto MatcherM = decl(anyOf(
225 objcMethodDecl(HasParamAndWritesInMarkedFuncM).bind(ID: IsMethodBind),
226 functionDecl(HasParamAndWritesInMarkedFuncM),
227 blockDecl(HasParamAndWritesInMarkedFuncM)));
228
229 auto Matches = match(Matcher: MatcherM, Node: *D, Context&: AM.getASTContext());
230 for (BoundNodes Match : Matches)
231 emitDiagnostics(Match, D, BR, AM, Checker: 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