Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
#include "UnhandledSelfAssignmentCheck.h"
#include "UnintendedCharOstreamOutputCheck.h"
#include "UniquePtrArrayMismatchCheck.h"
#include "UnsafeFormatStringCheck.h"
#include "UnsafeFunctionsCheck.h"
#include "UnsafeToAllowExceptionsCheck.h"
#include "UnusedLocalNonTrivialVariableCheck.h"
Expand Down Expand Up @@ -321,6 +322,8 @@ class BugproneModule : public ClangTidyModule {
"bugprone-unique-ptr-array-mismatch");
CheckFactories.registerCheck<CrtpConstructorAccessibilityCheck>(
"bugprone-crtp-constructor-accessibility");
CheckFactories.registerCheck<UnsafeFormatStringCheck>(
Comment thread
dkrupp marked this conversation as resolved.
Resolved
"bugprone-unsafe-format-string");
CheckFactories.registerCheck<UnsafeFunctionsCheck>(
"bugprone-unsafe-functions");
CheckFactories.registerCheck<UnsafeToAllowExceptionsCheck>(
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ add_clang_library(clangTidyBugproneModule STATIC
UnhandledExceptionAtNewCheck.cpp
UnhandledSelfAssignmentCheck.cpp
UniquePtrArrayMismatchCheck.cpp
UnsafeFormatStringCheck.cpp
Comment thread
dkrupp marked this conversation as resolved.
Resolved
UnsafeFunctionsCheck.cpp
UnsafeToAllowExceptionsCheck.cpp
UnusedLocalNonTrivialVariableCheck.cpp
Expand Down
273 changes: 273 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp
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;
Comment thread
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++;
}
}
Comment thread
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;
Comment thread
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 clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h
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
6 changes: 6 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ infrastructure are described first, followed by tool-specific sections.

#### New checks

- New {doc}`bugprone-unsafe-format-string
<clang-tidy/checks/bugprone/unsafe-format-string>` check.

Detects usage of vulnerable `printf` and `scanf`-like format string
functions with unbounded `%s` specifiers that can cause buffer overflows.

- New {doc}`llvm-invalid-regex-pattern
<clang-tidy/checks/llvm/invalid-regex-pattern>` check.

Expand Down
Loading
Loading