| 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 | |