1 | //===--- UnsafeFunctionsCheck.cpp - clang-tidy ----------------------------===// |
---|---|
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 | #include "UnsafeFunctionsCheck.h" |
10 | #include "../utils/OptionsUtils.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
13 | #include "clang/Lex/PPCallbacks.h" |
14 | #include "clang/Lex/Preprocessor.h" |
15 | #include <cassert> |
16 | |
17 | using namespace clang::ast_matchers; |
18 | using namespace llvm; |
19 | |
20 | namespace clang::tidy::bugprone { |
21 | |
22 | static constexpr llvm::StringLiteral OptionNameCustomFunctions = |
23 | "CustomFunctions"; |
24 | static constexpr llvm::StringLiteral OptionNameReportDefaultFunctions = |
25 | "ReportDefaultFunctions"; |
26 | static constexpr llvm::StringLiteral OptionNameReportMoreUnsafeFunctions = |
27 | "ReportMoreUnsafeFunctions"; |
28 | |
29 | static constexpr llvm::StringLiteral FunctionNamesWithAnnexKReplacementId = |
30 | "FunctionNamesWithAnnexKReplacement"; |
31 | static constexpr llvm::StringLiteral FunctionNamesId = "FunctionsNames"; |
32 | static constexpr llvm::StringLiteral AdditionalFunctionNamesId = |
33 | "AdditionalFunctionsNames"; |
34 | static constexpr llvm::StringLiteral CustomFunctionNamesId = |
35 | "CustomFunctionNames"; |
36 | static constexpr llvm::StringLiteral DeclRefId = "DRE"; |
37 | |
38 | static std::optional<std::string> |
39 | getAnnexKReplacementFor(StringRef FunctionName) { |
40 | return StringSwitch<std::string>(FunctionName) |
41 | .Case(S: "strlen", Value: "strnlen_s") |
42 | .Case(S: "wcslen", Value: "wcsnlen_s") |
43 | .Default(Value: (Twine{FunctionName} + "_s").str()); |
44 | } |
45 | |
46 | static StringRef getReplacementFor(StringRef FunctionName, |
47 | bool IsAnnexKAvailable) { |
48 | if (IsAnnexKAvailable) { |
49 | // Try to find a better replacement from Annex K first. |
50 | StringRef AnnexKReplacementFunction = |
51 | StringSwitch<StringRef>(FunctionName) |
52 | .Cases(S0: "asctime", S1: "asctime_r", Value: "asctime_s") |
53 | .Case(S: "gets", Value: "gets_s") |
54 | .Default(Value: {}); |
55 | if (!AnnexKReplacementFunction.empty()) |
56 | return AnnexKReplacementFunction; |
57 | } |
58 | |
59 | // FIXME: Some of these functions are available in C++ under "std::", and |
60 | // should be matched and suggested. |
61 | return StringSwitch<StringRef>(FunctionName) |
62 | .Cases(S0: "asctime", S1: "asctime_r", Value: "strftime") |
63 | .Case(S: "gets", Value: "fgets") |
64 | .Case(S: "rewind", Value: "fseek") |
65 | .Case(S: "setbuf", Value: "setvbuf"); |
66 | } |
67 | |
68 | static StringRef getReplacementForAdditional(StringRef FunctionName, |
69 | bool IsAnnexKAvailable) { |
70 | if (IsAnnexKAvailable) { |
71 | // Try to find a better replacement from Annex K first. |
72 | StringRef AnnexKReplacementFunction = StringSwitch<StringRef>(FunctionName) |
73 | .Case(S: "bcopy", Value: "memcpy_s") |
74 | .Case(S: "bzero", Value: "memset_s") |
75 | .Default(Value: {}); |
76 | |
77 | if (!AnnexKReplacementFunction.empty()) |
78 | return AnnexKReplacementFunction; |
79 | } |
80 | |
81 | return StringSwitch<StringRef>(FunctionName) |
82 | .Case(S: "bcmp", Value: "memcmp") |
83 | .Case(S: "bcopy", Value: "memcpy") |
84 | .Case(S: "bzero", Value: "memset") |
85 | .Case(S: "getpw", Value: "getpwuid") |
86 | .Case(S: "vfork", Value: "posix_spawn"); |
87 | } |
88 | |
89 | /// \returns The rationale for replacing the function \p FunctionName with the |
90 | /// safer alternative. |
91 | static StringRef getRationaleFor(StringRef FunctionName) { |
92 | return StringSwitch<StringRef>(FunctionName) |
93 | .Cases(S0: "asctime", S1: "asctime_r", S2: "ctime", |
94 | Value: "is not bounds-checking and non-reentrant") |
95 | .Cases(S0: "bcmp", S1: "bcopy", S2: "bzero", Value: "is deprecated") |
96 | .Cases(S0: "fopen", S1: "freopen", Value: "has no exclusive access to the opened file") |
97 | .Case(S: "gets", Value: "is insecure, was deprecated and removed in C11 and C++14") |
98 | .Case(S: "getpw", Value: "is dangerous as it may overflow the provided buffer") |
99 | .Cases(S0: "rewind", S1: "setbuf", Value: "has no error detection") |
100 | .Case(S: "vfork", Value: "is insecure as it can lead to denial of service " |
101 | "situations in the parent process") |
102 | .Default(Value: "is not bounds-checking"); |
103 | } |
104 | |
105 | /// Calculates whether Annex K is available for the current translation unit |
106 | /// based on the macro definitions and the language options. |
107 | /// |
108 | /// The result is cached and saved in \p CacheVar. |
109 | static bool isAnnexKAvailable(std::optional<bool> &CacheVar, Preprocessor *PP, |
110 | const LangOptions &LO) { |
111 | if (CacheVar.has_value()) |
112 | return *CacheVar; |
113 | |
114 | if (!LO.C11) |
115 | // TODO: How is "Annex K" available in C++ mode? |
116 | return (CacheVar = false).value(); |
117 | |
118 | assert(PP && "No Preprocessor registered."); |
119 | |
120 | if (!PP->isMacroDefined(Id: "__STDC_LIB_EXT1__") || |
121 | !PP->isMacroDefined(Id: "__STDC_WANT_LIB_EXT1__")) |
122 | return (CacheVar = false).value(); |
123 | |
124 | const auto *MI = |
125 | PP->getMacroInfo(II: PP->getIdentifierInfo(Name: "__STDC_WANT_LIB_EXT1__")); |
126 | if (!MI || MI->tokens_empty()) |
127 | return (CacheVar = false).value(); |
128 | |
129 | const Token &T = MI->tokens().back(); |
130 | if (!T.isLiteral() || !T.getLiteralData()) |
131 | return (CacheVar = false).value(); |
132 | |
133 | CacheVar = StringRef(T.getLiteralData(), T.getLength()) == "1"; |
134 | return CacheVar.value(); |
135 | } |
136 | |
137 | static std::vector<UnsafeFunctionsCheck::CheckedFunction> |
138 | parseCheckedFunctions(StringRef Option, ClangTidyContext *Context) { |
139 | const std::vector<StringRef> Functions = |
140 | utils::options::parseStringList(Option); |
141 | std::vector<UnsafeFunctionsCheck::CheckedFunction> Result; |
142 | Result.reserve(n: Functions.size()); |
143 | |
144 | for (StringRef Function : Functions) { |
145 | if (Function.empty()) |
146 | continue; |
147 | |
148 | const auto [Name, Rest] = Function.split(Separator: ','); |
149 | const auto [Replacement, Reason] = Rest.split(Separator: ','); |
150 | |
151 | if (Name.trim().empty()) { |
152 | Context->configurationDiag(Message: "invalid configuration value for option '%0'; " |
153 | "expected the name of an unsafe function") |
154 | << OptionNameCustomFunctions; |
155 | continue; |
156 | } |
157 | |
158 | Result.push_back( |
159 | x: {.Name: Name.trim().str(), |
160 | .Pattern: matchers::MatchesAnyListedNameMatcher::NameMatcher(Name.trim()), |
161 | .Replacement: Replacement.trim().str(), .Reason: Reason.trim().str()}); |
162 | } |
163 | |
164 | return Result; |
165 | } |
166 | |
167 | static std::string serializeCheckedFunctions( |
168 | const std::vector<UnsafeFunctionsCheck::CheckedFunction> &Functions) { |
169 | std::vector<std::string> Result; |
170 | Result.reserve(n: Functions.size()); |
171 | |
172 | for (const auto &Entry : Functions) { |
173 | if (Entry.Reason.empty()) |
174 | Result.push_back(x: Entry.Name + ","+ Entry.Replacement); |
175 | else |
176 | Result.push_back(x: Entry.Name + ","+ Entry.Replacement + ","+ |
177 | Entry.Reason); |
178 | } |
179 | |
180 | return llvm::join(R&: Result, Separator: ";"); |
181 | } |
182 | |
183 | UnsafeFunctionsCheck::UnsafeFunctionsCheck(StringRef Name, |
184 | ClangTidyContext *Context) |
185 | : ClangTidyCheck(Name, Context), |
186 | CustomFunctions(parseCheckedFunctions( |
187 | Option: Options.get(LocalName: OptionNameCustomFunctions, Default: ""), Context)), |
188 | ReportDefaultFunctions( |
189 | Options.get(LocalName: OptionNameReportDefaultFunctions, Default: true)), |
190 | ReportMoreUnsafeFunctions( |
191 | Options.get(LocalName: OptionNameReportMoreUnsafeFunctions, Default: true)) {} |
192 | |
193 | void UnsafeFunctionsCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
194 | Options.store(Options&: Opts, LocalName: OptionNameCustomFunctions, |
195 | Value: serializeCheckedFunctions(Functions: CustomFunctions)); |
196 | Options.store(Options&: Opts, LocalName: OptionNameReportDefaultFunctions, Value: ReportDefaultFunctions); |
197 | Options.store(Options&: Opts, LocalName: OptionNameReportMoreUnsafeFunctions, |
198 | Value: ReportMoreUnsafeFunctions); |
199 | } |
200 | |
201 | void UnsafeFunctionsCheck::registerMatchers(MatchFinder *Finder) { |
202 | if (ReportDefaultFunctions) { |
203 | if (getLangOpts().C11) { |
204 | // Matching functions with safe replacements only in Annex K. |
205 | auto FunctionNamesWithAnnexKReplacementMatcher = hasAnyName( |
206 | "::bsearch", "::ctime", "::fopen", "::fprintf", "::freopen", |
207 | "::fscanf", "::fwprintf", "::fwscanf", "::getenv", "::gmtime", |
208 | "::localtime", "::mbsrtowcs", "::mbstowcs", "::memcpy", "::memmove", |
209 | "::memset", "::printf", "::qsort", "::scanf", "::snprintf", |
210 | "::sprintf", "::sscanf", "::strcat", "::strcpy", "::strerror", |
211 | "::strlen", "::strncat", "::strncpy", "::strtok", "::swprintf", |
212 | "::swscanf", "::vfprintf", "::vfscanf", "::vfwprintf", "::vfwscanf", |
213 | "::vprintf", "::vscanf", "::vsnprintf", "::vsprintf", "::vsscanf", |
214 | "::vswprintf", "::vswscanf", "::vwprintf", "::vwscanf", "::wcrtomb", |
215 | "::wcscat", "::wcscpy", "::wcslen", "::wcsncat", "::wcsncpy", |
216 | "::wcsrtombs", "::wcstok", "::wcstombs", "::wctomb", "::wmemcpy", |
217 | "::wmemmove", "::wprintf", "::wscanf"); |
218 | Finder->addMatcher( |
219 | NodeMatch: declRefExpr(to(InnerMatcher: functionDecl(FunctionNamesWithAnnexKReplacementMatcher) |
220 | .bind(ID: FunctionNamesWithAnnexKReplacementId))) |
221 | .bind(ID: DeclRefId), |
222 | Action: this); |
223 | } |
224 | |
225 | // Matching functions with replacements without Annex K. |
226 | auto FunctionNamesMatcher = |
227 | hasAnyName("::asctime", "asctime_r", "::gets", "::rewind", "::setbuf"); |
228 | Finder->addMatcher( |
229 | NodeMatch: declRefExpr( |
230 | to(InnerMatcher: functionDecl(FunctionNamesMatcher).bind(ID: FunctionNamesId))) |
231 | .bind(ID: DeclRefId), |
232 | Action: this); |
233 | |
234 | if (ReportMoreUnsafeFunctions) { |
235 | // Matching functions with replacements without Annex K, at user request. |
236 | auto AdditionalFunctionNamesMatcher = |
237 | hasAnyName("::bcmp", "::bcopy", "::bzero", "::getpw", "::vfork"); |
238 | Finder->addMatcher( |
239 | NodeMatch: declRefExpr(to(InnerMatcher: functionDecl(AdditionalFunctionNamesMatcher) |
240 | .bind(ID: AdditionalFunctionNamesId))) |
241 | .bind(ID: DeclRefId), |
242 | Action: this); |
243 | } |
244 | } |
245 | |
246 | if (!CustomFunctions.empty()) { |
247 | std::vector<llvm::StringRef> FunctionNames; |
248 | FunctionNames.reserve(n: CustomFunctions.size()); |
249 | |
250 | for (const auto &Entry : CustomFunctions) |
251 | FunctionNames.push_back(x: Entry.Name); |
252 | |
253 | auto CustomFunctionsMatcher = matchers::matchesAnyListedName(NameList: FunctionNames); |
254 | |
255 | Finder->addMatcher(NodeMatch: declRefExpr(to(InnerMatcher: functionDecl(CustomFunctionsMatcher) |
256 | .bind(ID: CustomFunctionNamesId))) |
257 | .bind(ID: DeclRefId), |
258 | Action: this); |
259 | // C++ member calls do not contain a DeclRefExpr to the function decl. |
260 | // Instead, they contain a MemberExpr that refers to the decl. |
261 | Finder->addMatcher(NodeMatch: memberExpr(member(InnerMatcher: functionDecl(CustomFunctionsMatcher) |
262 | .bind(ID: CustomFunctionNamesId))) |
263 | .bind(ID: DeclRefId), |
264 | Action: this); |
265 | } |
266 | } |
267 | |
268 | void UnsafeFunctionsCheck::check(const MatchFinder::MatchResult &Result) { |
269 | const Expr *SourceExpr; |
270 | const FunctionDecl *FuncDecl; |
271 | |
272 | if (const auto *DeclRef = Result.Nodes.getNodeAs<DeclRefExpr>(ID: DeclRefId)) { |
273 | SourceExpr = DeclRef; |
274 | FuncDecl = cast<FunctionDecl>(Val: DeclRef->getDecl()); |
275 | } else if (const auto *Member = |
276 | Result.Nodes.getNodeAs<MemberExpr>(ID: DeclRefId)) { |
277 | SourceExpr = Member; |
278 | FuncDecl = cast<FunctionDecl>(Val: Member->getMemberDecl()); |
279 | } else { |
280 | llvm_unreachable("No valid matched node in check()"); |
281 | return; |
282 | } |
283 | |
284 | assert(SourceExpr && FuncDecl && "No valid matched node in check()"); |
285 | |
286 | // Only one of these are matched at a time. |
287 | const auto *AnnexK = Result.Nodes.getNodeAs<FunctionDecl>( |
288 | ID: FunctionNamesWithAnnexKReplacementId); |
289 | const auto *Normal = Result.Nodes.getNodeAs<FunctionDecl>(ID: FunctionNamesId); |
290 | const auto *Additional = |
291 | Result.Nodes.getNodeAs<FunctionDecl>(ID: AdditionalFunctionNamesId); |
292 | const auto *Custom = |
293 | Result.Nodes.getNodeAs<FunctionDecl>(ID: CustomFunctionNamesId); |
294 | assert((AnnexK || Normal || Additional || Custom) && |
295 | "No valid match category."); |
296 | |
297 | bool AnnexKIsAvailable = |
298 | isAnnexKAvailable(CacheVar&: IsAnnexKAvailable, PP, LO: getLangOpts()); |
299 | StringRef FunctionName = FuncDecl->getName(); |
300 | |
301 | if (Custom) { |
302 | for (const auto &Entry : CustomFunctions) { |
303 | if (Entry.Pattern.match(*FuncDecl)) { |
304 | StringRef Reason = |
305 | Entry.Reason.empty() ? "is marked as unsafe": Entry.Reason.c_str(); |
306 | |
307 | if (Entry.Replacement.empty()) { |
308 | diag(Loc: SourceExpr->getExprLoc(), |
309 | Description: "function %0 %1; it should not be used") |
310 | << FuncDecl << Reason << Entry.Replacement |
311 | << SourceExpr->getSourceRange(); |
312 | } else { |
313 | diag(Loc: SourceExpr->getExprLoc(), |
314 | Description: "function %0 %1; '%2' should be used instead") |
315 | << FuncDecl << Reason << Entry.Replacement |
316 | << SourceExpr->getSourceRange(); |
317 | } |
318 | |
319 | return; |
320 | } |
321 | } |
322 | |
323 | llvm_unreachable("No custom function was matched."); |
324 | return; |
325 | } |
326 | |
327 | const std::optional<std::string> ReplacementFunctionName = |
328 | [&]() -> std::optional<std::string> { |
329 | if (AnnexK) { |
330 | if (AnnexKIsAvailable) |
331 | return getAnnexKReplacementFor(FunctionName); |
332 | return std::nullopt; |
333 | } |
334 | |
335 | if (Normal) |
336 | return getReplacementFor(FunctionName, IsAnnexKAvailable: AnnexKIsAvailable).str(); |
337 | |
338 | if (Additional) |
339 | return getReplacementForAdditional(FunctionName, IsAnnexKAvailable: AnnexKIsAvailable).str(); |
340 | |
341 | llvm_unreachable("Unhandled match category"); |
342 | }(); |
343 | if (!ReplacementFunctionName) |
344 | return; |
345 | |
346 | diag(Loc: SourceExpr->getExprLoc(), Description: "function %0 %1; '%2' should be used instead") |
347 | << FuncDecl << getRationaleFor(FunctionName) |
348 | << ReplacementFunctionName.value() << SourceExpr->getSourceRange(); |
349 | } |
350 | |
351 | void UnsafeFunctionsCheck::registerPPCallbacks( |
352 | const SourceManager &SM, Preprocessor *PP, |
353 | Preprocessor * /*ModuleExpanderPP*/) { |
354 | this->PP = PP; |
355 | } |
356 | |
357 | void UnsafeFunctionsCheck::onEndOfTranslationUnit() { |
358 | this->PP = nullptr; |
359 | IsAnnexKAvailable.reset(); |
360 | } |
361 | |
362 | } // namespace clang::tidy::bugprone |
363 |
Definitions
- OptionNameCustomFunctions
- OptionNameReportDefaultFunctions
- OptionNameReportMoreUnsafeFunctions
- FunctionNamesWithAnnexKReplacementId
- FunctionNamesId
- AdditionalFunctionNamesId
- CustomFunctionNamesId
- DeclRefId
- getAnnexKReplacementFor
- getReplacementFor
- getReplacementForAdditional
- getRationaleFor
- isAnnexKAvailable
- parseCheckedFunctions
- serializeCheckedFunctions
- UnsafeFunctionsCheck
- storeOptions
- registerMatchers
- check
- registerPPCallbacks
Update your C++ knowledge – Modern C++11/14/17 Training
Find out more