Repository navigation
[clang-tidy] New bugprone-unsafe-format-string check #168691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dkrupp
wants to merge
25
commits into
llvm:main
Choose a base branch
from
dkrupp:bugprone-unsafe-format-string-pr
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+916
−0
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
03433ee
[clang-tidy] New bugprone-unsafe-format-string check
dkrupp cefe274
Fixup formmatting
dkrupp c5b8b1b
Fixup. Fixing clang-tidy finding
dkrupp e88df90
Review fixes
dkrupp 272e66e
Add configuration options to the checker
dkrupp 6fa63af
fixup
dkrupp ab4bf4a
fixup
dkrupp 86aab93
Extend Release Notes
dkrupp 52b9826
Fix linter error
dkrupp 5626c34
Fixups
dkrupp c760a3d
Merge remote-tracking branch 'upstream/main' into bugprone-unsafe-for…
dkrupp 3c1f63b
Transform the docs to md.
dkrupp 40f00bc
Fixup
dkrupp 3b191dd
fixup!
dkrupp b56539a
Linter fixup!
dkrupp 2ae0701
Fixing review comments
dkrupp fca2acb
Merge remote-tracking branch 'upstream/main' into bugprone-unsafe-for…
dkrupp 1a7a457
Remove unrelated doc changes
dkrupp d30722d
Linter and formatting fixes
dkrupp 0acc0ea
fixup
dkrupp 6ca8567
Fixup!
dkrupp e329f33
formatting fix
dkrupp 9625faa
Fixup!
dkrupp c1c3065
formatting fix
dkrupp 1a66725
fixup
dkrupp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
273 changes: 273 additions & 0 deletions
273
clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #include "UnsafeFormatStringCheck.h" | ||
| #include "../utils/OptionsUtils.h" | ||
| #include "clang/ASTMatchers/ASTMatchFinder.h" | ||
| #include "llvm/ADT/StringRef.h" | ||
| #include "llvm/Support/ConvertUTF.h" | ||
| #include "llvm/Support/ErrorHandling.h" | ||
| #include "llvm/Support/FormatVariadic.h" | ||
| #include "llvm/Support/Regex.h" | ||
| #include <string> | ||
|
|
||
| using namespace clang::ast_matchers; | ||
|
|
||
| namespace clang::tidy::bugprone { | ||
|
|
||
| static constexpr StringRef OptionNameCustomPrintfFunctions = | ||
| "CustomPrintfFunctions"; | ||
| static constexpr StringRef OptionNameCustomScanfFunctions = | ||
| "CustomScanfFunctions"; | ||
|
|
||
| static constexpr StringRef BuiltInFormatBind = "format"; | ||
| static constexpr StringRef BuiltInCallBind = "call"; | ||
| static constexpr StringRef PrintfCallBind = "printfcall"; | ||
| static constexpr StringRef ScanfCallBind = "scanfcall"; | ||
|
|
||
| static std::vector<UnsafeFormatStringCheck::CheckedFunction> | ||
| parseCheckedFunctions(StringRef OptionName, StringRef Option, | ||
| ClangTidyContext *Context) { | ||
| const std::vector<StringRef> Functions = | ||
| utils::options::parseStringList(Option); | ||
| std::vector<UnsafeFormatStringCheck::CheckedFunction> Result; | ||
| Result.reserve(Functions.size()); | ||
|
|
||
| for (const StringRef Function : Functions) { | ||
| if (Function.empty()) | ||
| continue; | ||
| const auto [Name, ParamCount] = Function.split(','); | ||
| unsigned long Count = 0; | ||
| if (Name.trim().empty() || ParamCount.trim().empty() || | ||
| ParamCount.trim().getAsInteger(10, Count)) { | ||
| Context->configurationDiag( | ||
| "invalid configuration value for option '%0'; " | ||
| "expected '<functionname>, <paramcount>;' pairs") | ||
| << OptionName; | ||
| continue; | ||
| } | ||
| Result.push_back( | ||
| {Name.trim().str(), | ||
| matchers::MatchesAnyListedRegexNameMatcher::NameMatcher(Name.trim()), | ||
| Count}); | ||
| } | ||
|
|
||
| return Result; | ||
| } | ||
|
|
||
| void UnsafeFormatStringCheck::registerFunctionMatchers( | ||
| MatchFinder *Finder, const std::vector<CheckedFunction> &Funcs, | ||
| StringRef BindTo) { | ||
| std::vector<llvm::StringRef> FunctionNames; | ||
| FunctionNames.reserve(Funcs.size()); | ||
| for (const CheckedFunction &Entry : Funcs) | ||
| FunctionNames.emplace_back(Entry.Name); | ||
| const auto CustomFunctionsMatcher = | ||
| matchers::matchesAnyListedRegexName(FunctionNames); | ||
| Finder->addMatcher( | ||
| callExpr(callee(functionDecl(CustomFunctionsMatcher))).bind(BindTo), | ||
| this); | ||
| } | ||
|
|
||
| UnsafeFormatStringCheck::UnsafeFormatStringCheck(StringRef Name, | ||
| ClangTidyContext *Context) | ||
| : ClangTidyCheck(Name, Context), | ||
| CustomPrintfFunctions(parseCheckedFunctions( | ||
| OptionNameCustomPrintfFunctions, | ||
| Options.get(OptionNameCustomPrintfFunctions, ""), Context)), | ||
| CustomScanfFunctions(parseCheckedFunctions( | ||
| OptionNameCustomScanfFunctions, | ||
| Options.get(OptionNameCustomScanfFunctions, ""), Context)) {} | ||
|
|
||
| void UnsafeFormatStringCheck::registerMatchers(MatchFinder *Finder) { | ||
| // Matches sprintf and scanf family functions in std namespace in C++ and | ||
| // globally in C. | ||
| const auto VulnerableFunctionsArg0 = | ||
| hasAnyName("scanf", "vscanf", "wscanf", "vwscanf"); | ||
| const auto VulnerableFunctionsArg1 = | ||
| hasAnyName("sprintf", "vsprintf", "fscanf", "sscanf", "vfscanf", | ||
| "vsscanf", "fwscanf", "swscanf", "vfwscanf", "vswscanf"); | ||
| Finder->addMatcher( | ||
| callExpr( | ||
| callee(functionDecl(VulnerableFunctionsArg0, | ||
| anyOf(isInStdNamespace(), | ||
| hasDeclContext(translationUnitDecl())))), | ||
| hasArgument(0, stringLiteral().bind(BuiltInFormatBind))) | ||
| .bind(BuiltInCallBind), | ||
| this); | ||
|
|
||
| Finder->addMatcher( | ||
| callExpr( | ||
| callee(functionDecl(VulnerableFunctionsArg1, | ||
| anyOf(isInStdNamespace(), | ||
| hasDeclContext(translationUnitDecl())))), | ||
| hasArgument(1, stringLiteral().bind(BuiltInFormatBind))) | ||
| .bind(BuiltInCallBind), | ||
| this); | ||
|
|
||
| if (!CustomPrintfFunctions.empty()) | ||
| registerFunctionMatchers(Finder, CustomPrintfFunctions, PrintfCallBind); | ||
| if (!CustomScanfFunctions.empty()) | ||
| registerFunctionMatchers(Finder, CustomScanfFunctions, ScanfCallBind); | ||
| } | ||
|
|
||
| std::string UnsafeFormatStringCheck::serializeConfig( | ||
| const std::vector<CheckedFunction> &Functions) { | ||
| std::vector<std::string> Result; | ||
| Result.reserve(Functions.size()); | ||
| for (const auto &Entry : Functions) | ||
| Result.push_back(Entry.Name + "," + | ||
| std::to_string(Entry.FormatStringLocation)); | ||
| return llvm::join(Result, ";"); | ||
| } | ||
|
|
||
| void UnsafeFormatStringCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { | ||
| Options.store(Opts, OptionNameCustomPrintfFunctions, | ||
| serializeConfig(CustomPrintfFunctions)); | ||
| Options.store(Opts, OptionNameCustomScanfFunctions, | ||
| serializeConfig(CustomScanfFunctions)); | ||
| } | ||
|
|
||
| const StringLiteral *UnsafeFormatStringCheck::getFormatLiteral( | ||
| const CallExpr *Call, const std::vector<CheckedFunction> &CustomFunctions) { | ||
| const FunctionDecl *FD = Call->getDirectCallee(); | ||
| if (!FD) | ||
| return nullptr; | ||
| for (const CheckedFunction &Entry : CustomFunctions) { | ||
| if (Entry.Pattern.match(*FD)) { | ||
| if (Entry.FormatStringLocation >= Call->getNumArgs()) | ||
| return nullptr; | ||
| const Expr *Arg = | ||
| Call->getArg(Entry.FormatStringLocation)->IgnoreImpCasts(); | ||
| return dyn_cast<StringLiteral>(Arg); | ||
| } | ||
| } | ||
| llvm_unreachable( | ||
| "The Call must match one of the functions in CustomFunctions."); | ||
| } | ||
|
|
||
| static bool hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily) { | ||
| size_t Pos = 0; | ||
| const size_t N = Fmt.size(); | ||
| while ((Pos = Fmt.find('%', Pos)) != StringRef::npos) { | ||
| if (Pos + 1 >= N) | ||
| break; | ||
|
|
||
| // Skip %% | ||
| if (Fmt[Pos + 1] == '%') { | ||
| Pos += 2; | ||
| continue; | ||
| } | ||
|
|
||
| size_t SpecPos = Pos + 1; | ||
|
|
||
| // Skip flags | ||
| while (SpecPos < N && | ||
| (Fmt[SpecPos] == '-' || Fmt[SpecPos] == '+' || Fmt[SpecPos] == ' ' || | ||
| Fmt[SpecPos] == '#' || Fmt[SpecPos] == '0')) { | ||
| SpecPos++; | ||
| } | ||
|
|
||
| // Check for field width | ||
| bool HasFieldWidth = false; | ||
| if (SpecPos < N && Fmt[SpecPos] == '*') { | ||
| HasFieldWidth = true; | ||
| SpecPos++; | ||
| } else { | ||
| while (SpecPos < N && isdigit(Fmt[SpecPos])) { | ||
| HasFieldWidth = true; | ||
| SpecPos++; | ||
| } | ||
| } | ||
|
|
||
| // Check for precision | ||
| bool HasPrecision = false; | ||
|
dkrupp marked this conversation as resolved.
Resolved
|
||
| if (SpecPos < N && Fmt[SpecPos] == '.') { | ||
| HasPrecision = true; | ||
| SpecPos++; | ||
| if (SpecPos < N && Fmt[SpecPos] == '*') { | ||
| // precision in argument | ||
| SpecPos++; | ||
| } else { | ||
| while (SpecPos < N && isdigit(Fmt[SpecPos])) | ||
| SpecPos++; | ||
| } | ||
| } | ||
|
NagyDonat marked this conversation as resolved.
Resolved
|
||
|
|
||
| // Skip length modifiers | ||
| while (SpecPos < N && (Fmt[SpecPos] == 'h' || Fmt[SpecPos] == 'l' || | ||
| Fmt[SpecPos] == 'L' || Fmt[SpecPos] == 'z' || | ||
| Fmt[SpecPos] == 'j' || Fmt[SpecPos] == 't')) { | ||
| SpecPos++; | ||
| } | ||
|
|
||
| // Check for 's' specifier | ||
| if (SpecPos < N && Fmt[SpecPos] == 's') { | ||
| if (IsScanfFamily) { | ||
| // For scanf family, field width provides protection | ||
| if (!HasFieldWidth) | ||
| return true; | ||
| } else { | ||
| // For sprintf family, only precision provides protection | ||
| if (!HasPrecision) | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| Pos = SpecPos + 1; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { | ||
| const CallExpr *Call = nullptr; | ||
| const StringLiteral *Format = nullptr; | ||
| bool IsScanfFamily = false; | ||
| if ((Call = Result.Nodes.getNodeAs<CallExpr>(BuiltInCallBind))) { | ||
| Format = Result.Nodes.getNodeAs<StringLiteral>(BuiltInFormatBind); | ||
| const auto *Callee = cast<FunctionDecl>(Call->getCalleeDecl()); | ||
| IsScanfFamily = Callee->getName().contains("scanf"); | ||
| } else if ((Call = Result.Nodes.getNodeAs<CallExpr>(PrintfCallBind))) { | ||
| Format = | ||
| UnsafeFormatStringCheck::getFormatLiteral(Call, CustomPrintfFunctions); | ||
| IsScanfFamily = false; | ||
| } else if ((Call = Result.Nodes.getNodeAs<CallExpr>(ScanfCallBind))) { | ||
| Format = | ||
| UnsafeFormatStringCheck::getFormatLiteral(Call, CustomScanfFunctions); | ||
| IsScanfFamily = true; | ||
| } else { | ||
| llvm_unreachable("No valid matched node in check()"); | ||
| } | ||
|
|
||
| // Format can be null with incorrect format string position configuration | ||
| if (!Format) | ||
| return; | ||
|
vbvictor marked this conversation as resolved.
Resolved
|
||
|
|
||
| std::string FormatString; | ||
| if (Format->getCharByteWidth() == 1) { | ||
| FormatString = Format->getString().str(); | ||
| } else if (Format->getCharByteWidth() == 2) { | ||
| // Handle wide strings by converting to narrow string for analysis | ||
| convertUTF16ToUTF8String(Format->getBytes(), FormatString); | ||
| } else if (Format->getCharByteWidth() == 4) { | ||
| // Handle wide strings by converting to narrow string for analysis | ||
| convertUTF32ToUTF8String(Format->getBytes(), FormatString); | ||
| } | ||
|
|
||
| if (!hasUnboundedStringSpecifier(FormatString, IsScanfFamily)) | ||
| return; | ||
| diag(Call->getBeginLoc(), | ||
| "format specifier '%%s' without %select{precision|field width}0 may " | ||
| "cause buffer overflow; " | ||
| "consider using '%%%select{.|}0Ns' where N limits " | ||
| "%select{output|input}0 length") | ||
| << IsScanfFamily << Call->getSourceRange(); | ||
| } | ||
|
|
||
| } // namespace clang::tidy::bugprone | ||
49 changes: 49 additions & 0 deletions
49
clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| //===------------------------------------------------------------*- C++ -*-===// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFORMATSTRINGCHECK_H | ||
| #define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFORMATSTRINGCHECK_H | ||
|
|
||
| #include "../ClangTidyCheck.h" | ||
| #include "../utils/Matchers.h" | ||
|
|
||
| namespace clang::tidy::bugprone { | ||
|
|
||
| /// Detects usage of vulnerable format string functions with unbounded %s | ||
| /// specifiers that can cause buffer overflows. | ||
| /// | ||
| /// For the user-facing documentation see: | ||
| /// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unsafe-format-string.html | ||
| class UnsafeFormatStringCheck : public ClangTidyCheck { | ||
| public: | ||
| UnsafeFormatStringCheck(StringRef Name, ClangTidyContext *Context); | ||
| void storeOptions(ClangTidyOptions::OptionMap &Opts) override; | ||
| void registerMatchers(ast_matchers::MatchFinder *Finder) override; | ||
| void check(const ast_matchers::MatchFinder::MatchResult &Result) override; | ||
|
|
||
| struct CheckedFunction { | ||
| std::string Name; | ||
| matchers::MatchesAnyListedRegexNameMatcher::NameMatcher Pattern; | ||
| unsigned long FormatStringLocation; | ||
| }; | ||
|
|
||
| private: | ||
| const std::vector<CheckedFunction> CustomPrintfFunctions; | ||
| const std::vector<CheckedFunction> CustomScanfFunctions; | ||
| void registerFunctionMatchers(ast_matchers::MatchFinder *Finder, | ||
| const std::vector<CheckedFunction> &Funcs, | ||
| StringRef BindTo); | ||
| std::string serializeConfig(const std::vector<CheckedFunction> &Functions); | ||
| static const StringLiteral * | ||
| getFormatLiteral(const CallExpr *, | ||
| const std::vector<CheckedFunction> &CustomFunctions); | ||
| }; | ||
|
|
||
| } // namespace clang::tidy::bugprone | ||
|
|
||
| #endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFORMATSTRINGCHECK_H |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.