Skip to content

[clang-tidy] Add user-defined functions to bugprone-unsafe-functions check #106350

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

Merged
merged 16 commits into from
Sep 26, 2024
Merged
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
178 changes: 143 additions & 35 deletions clang-tools-extra/clang-tidy/bugprone/UnsafeFunctionsCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//

#include "UnsafeFunctionsCheck.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/PPCallbacks.h"
Expand All @@ -18,6 +19,10 @@ using namespace llvm;

namespace clang::tidy::bugprone {

static constexpr llvm::StringLiteral OptionNameCustomFunctions =
"CustomFunctions";
static constexpr llvm::StringLiteral OptionNameReportDefaultFunctions =
"ReportDefaultFunctions";
static constexpr llvm::StringLiteral OptionNameReportMoreUnsafeFunctions =
"ReportMoreUnsafeFunctions";

Expand All @@ -26,6 +31,8 @@ static constexpr llvm::StringLiteral FunctionNamesWithAnnexKReplacementId =
static constexpr llvm::StringLiteral FunctionNamesId = "FunctionsNames";
static constexpr llvm::StringLiteral AdditionalFunctionNamesId =
"AdditionalFunctionsNames";
static constexpr llvm::StringLiteral CustomFunctionNamesId =
"CustomFunctionNames";
static constexpr llvm::StringLiteral DeclRefId = "DRE";

static std::optional<std::string>
Expand Down Expand Up @@ -127,57 +134,128 @@ static bool isAnnexKAvailable(std::optional<bool> &CacheVar, Preprocessor *PP,
return CacheVar.value();
}

static std::vector<UnsafeFunctionsCheck::CheckedFunction>
parseCheckedFunctions(StringRef Option, ClangTidyContext *Context) {
const std::vector<StringRef> Functions =
utils::options::parseStringList(Option);
std::vector<UnsafeFunctionsCheck::CheckedFunction> Result;
Result.reserve(Functions.size());

for (StringRef Function : Functions) {
if (Function.empty())
continue;

const auto [Name, Rest] = Function.split(',');
const auto [Replacement, Reason] = Rest.split(',');

if (Name.trim().empty()) {
Context->configurationDiag("invalid configuration value for option '%0'; "
"expected the name of an unsafe function")
<< OptionNameCustomFunctions;
continue;
}

Result.push_back(
{Name.trim().str(),
matchers::MatchesAnyListedNameMatcher::NameMatcher(Name.trim()),
Replacement.trim().str(), Reason.trim().str()});
}

return Result;
}

static std::string serializeCheckedFunctions(
const std::vector<UnsafeFunctionsCheck::CheckedFunction> &Functions) {
std::vector<std::string> Result;
Result.reserve(Functions.size());

for (const auto &Entry : Functions) {
if (Entry.Reason.empty())
Result.push_back(Entry.Name + "," + Entry.Replacement);
else
Result.push_back(Entry.Name + "," + Entry.Replacement + "," +
Entry.Reason);
}

return llvm::join(Result, ";");
}

UnsafeFunctionsCheck::UnsafeFunctionsCheck(StringRef Name,
ClangTidyContext *Context)
: ClangTidyCheck(Name, Context),
CustomFunctions(parseCheckedFunctions(
Options.get(OptionNameCustomFunctions, ""), Context)),
ReportDefaultFunctions(
Options.get(OptionNameReportDefaultFunctions, true)),
ReportMoreUnsafeFunctions(
Options.get(OptionNameReportMoreUnsafeFunctions, true)) {}

void UnsafeFunctionsCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, OptionNameCustomFunctions,
serializeCheckedFunctions(CustomFunctions));
Options.store(Opts, OptionNameReportDefaultFunctions, ReportDefaultFunctions);
Options.store(Opts, OptionNameReportMoreUnsafeFunctions,
ReportMoreUnsafeFunctions);
}

void UnsafeFunctionsCheck::registerMatchers(MatchFinder *Finder) {
if (getLangOpts().C11) {
// Matching functions with safe replacements only in Annex K.
auto FunctionNamesWithAnnexKReplacementMatcher = hasAnyName(
"::bsearch", "::ctime", "::fopen", "::fprintf", "::freopen", "::fscanf",
"::fwprintf", "::fwscanf", "::getenv", "::gmtime", "::localtime",
"::mbsrtowcs", "::mbstowcs", "::memcpy", "::memmove", "::memset",
"::printf", "::qsort", "::scanf", "::snprintf", "::sprintf", "::sscanf",
"::strcat", "::strcpy", "::strerror", "::strlen", "::strncat",
"::strncpy", "::strtok", "::swprintf", "::swscanf", "::vfprintf",
"::vfscanf", "::vfwprintf", "::vfwscanf", "::vprintf", "::vscanf",
"::vsnprintf", "::vsprintf", "::vsscanf", "::vswprintf", "::vswscanf",
"::vwprintf", "::vwscanf", "::wcrtomb", "::wcscat", "::wcscpy",
"::wcslen", "::wcsncat", "::wcsncpy", "::wcsrtombs", "::wcstok",
"::wcstombs", "::wctomb", "::wmemcpy", "::wmemmove", "::wprintf",
"::wscanf");
if (ReportDefaultFunctions) {
if (getLangOpts().C11) {
// Matching functions with safe replacements only in Annex K.
auto FunctionNamesWithAnnexKReplacementMatcher = hasAnyName(
"::bsearch", "::ctime", "::fopen", "::fprintf", "::freopen",
"::fscanf", "::fwprintf", "::fwscanf", "::getenv", "::gmtime",
"::localtime", "::mbsrtowcs", "::mbstowcs", "::memcpy", "::memmove",
"::memset", "::printf", "::qsort", "::scanf", "::snprintf",
"::sprintf", "::sscanf", "::strcat", "::strcpy", "::strerror",
"::strlen", "::strncat", "::strncpy", "::strtok", "::swprintf",
"::swscanf", "::vfprintf", "::vfscanf", "::vfwprintf", "::vfwscanf",
"::vprintf", "::vscanf", "::vsnprintf", "::vsprintf", "::vsscanf",
"::vswprintf", "::vswscanf", "::vwprintf", "::vwscanf", "::wcrtomb",
"::wcscat", "::wcscpy", "::wcslen", "::wcsncat", "::wcsncpy",
"::wcsrtombs", "::wcstok", "::wcstombs", "::wctomb", "::wmemcpy",
"::wmemmove", "::wprintf", "::wscanf");
Finder->addMatcher(
declRefExpr(to(functionDecl(FunctionNamesWithAnnexKReplacementMatcher)
.bind(FunctionNamesWithAnnexKReplacementId)))
.bind(DeclRefId),
this);
}

// Matching functions with replacements without Annex K.
auto FunctionNamesMatcher =
hasAnyName("::asctime", "asctime_r", "::gets", "::rewind", "::setbuf");
Finder->addMatcher(
declRefExpr(to(functionDecl(FunctionNamesWithAnnexKReplacementMatcher)
.bind(FunctionNamesWithAnnexKReplacementId)))
declRefExpr(
to(functionDecl(FunctionNamesMatcher).bind(FunctionNamesId)))
.bind(DeclRefId),
this);

if (ReportMoreUnsafeFunctions) {
// Matching functions with replacements without Annex K, at user request.
auto AdditionalFunctionNamesMatcher =
hasAnyName("::bcmp", "::bcopy", "::bzero", "::getpw", "::vfork");
Finder->addMatcher(
declRefExpr(to(functionDecl(AdditionalFunctionNamesMatcher)
.bind(AdditionalFunctionNamesId)))
.bind(DeclRefId),
this);
}
}

// Matching functions with replacements without Annex K.
auto FunctionNamesMatcher =
hasAnyName("::asctime", "asctime_r", "::gets", "::rewind", "::setbuf");
Finder->addMatcher(
declRefExpr(to(functionDecl(FunctionNamesMatcher).bind(FunctionNamesId)))
.bind(DeclRefId),
this);

if (ReportMoreUnsafeFunctions) {
// Matching functions with replacements without Annex K, at user request.
auto AdditionalFunctionNamesMatcher =
hasAnyName("::bcmp", "::bcopy", "::bzero", "::getpw", "::vfork");
Finder->addMatcher(
declRefExpr(to(functionDecl(AdditionalFunctionNamesMatcher)
.bind(AdditionalFunctionNamesId)))
.bind(DeclRefId),
this);
if (!CustomFunctions.empty()) {
std::vector<llvm::StringRef> FunctionNames;
FunctionNames.reserve(CustomFunctions.size());

for (const auto &Entry : CustomFunctions)
FunctionNames.push_back(Entry.Name);

auto CustomFunctionsMatcher = matchers::matchesAnyListedName(FunctionNames);

Finder->addMatcher(declRefExpr(to(functionDecl(CustomFunctionsMatcher)
.bind(CustomFunctionNamesId)))
.bind(DeclRefId),
this);
}
}

Expand All @@ -186,16 +264,46 @@ void UnsafeFunctionsCheck::check(const MatchFinder::MatchResult &Result) {
const auto *FuncDecl = cast<FunctionDecl>(DeclRef->getDecl());
assert(DeclRef && FuncDecl && "No valid matched node in check()");

// Only one of these are matched at a time.
const auto *AnnexK = Result.Nodes.getNodeAs<FunctionDecl>(
FunctionNamesWithAnnexKReplacementId);
const auto *Normal = Result.Nodes.getNodeAs<FunctionDecl>(FunctionNamesId);
const auto *Additional =
Result.Nodes.getNodeAs<FunctionDecl>(AdditionalFunctionNamesId);
assert((AnnexK || Normal || Additional) && "No valid match category.");
const auto *Custom =
Result.Nodes.getNodeAs<FunctionDecl>(CustomFunctionNamesId);
assert((AnnexK || Normal || Additional || Custom) &&
"No valid match category.");

bool AnnexKIsAvailable =
isAnnexKAvailable(IsAnnexKAvailable, PP, getLangOpts());
StringRef FunctionName = FuncDecl->getName();

if (Custom) {
for (const auto &Entry : CustomFunctions) {
if (Entry.Pattern.match(*FuncDecl)) {
StringRef Reason =
Entry.Reason.empty() ? "is marked as unsafe" : Entry.Reason.c_str();

if (Entry.Replacement.empty()) {
diag(DeclRef->getExprLoc(), "function %0 %1; it should not be used")
<< FuncDecl << Reason << Entry.Replacement
<< DeclRef->getSourceRange();
} else {
diag(DeclRef->getExprLoc(),
"function %0 %1; '%2' should be used instead")
<< FuncDecl << Reason << Entry.Replacement
<< DeclRef->getSourceRange();
}

return;
}
}

llvm_unreachable("No custom function was matched.");
return;
}

const std::optional<std::string> ReplacementFunctionName =
[&]() -> std::optional<std::string> {
if (AnnexK) {
Expand Down
12 changes: 12 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/UnsafeFunctionsCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFUNCTIONSCHECK_H

#include "../ClangTidyCheck.h"
#include "../utils/Matchers.h"
#include <optional>

namespace clang::tidy::bugprone {
Expand All @@ -32,7 +33,18 @@ class UnsafeFunctionsCheck : public ClangTidyCheck {
Preprocessor *ModuleExpanderPP) override;
void onEndOfTranslationUnit() override;

struct CheckedFunction {
std::string Name;
matchers::MatchesAnyListedNameMatcher::NameMatcher Pattern;
std::string Replacement;
std::string Reason;
};

private:
const std::vector<CheckedFunction> CustomFunctions;

// If true, the default set of functions are reported.
const bool ReportDefaultFunctions;
/// If true, additional functions from widely used API-s (such as POSIX) are
/// added to the list of reported functions.
const bool ReportMoreUnsafeFunctions;
Expand Down
17 changes: 9 additions & 8 deletions clang-tools-extra/clang-tidy/utils/Matchers.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,7 @@ class MatchesAnyListedNameMatcher
NameList.begin(), NameList.end(), std::back_inserter(NameMatchers),
[](const llvm::StringRef Name) { return NameMatcher(Name); });
}
bool matches(
const NamedDecl &Node, ast_matchers::internal::ASTMatchFinder *Finder,
ast_matchers::internal::BoundNodesTreeBuilder *Builder) const override {
return llvm::any_of(NameMatchers, [&Node](const NameMatcher &NM) {
return NM.match(Node);
});
}

private:
class NameMatcher {
llvm::Regex Regex;
enum class MatchMode {
Expand Down Expand Up @@ -136,6 +128,15 @@ class MatchesAnyListedNameMatcher
}
};

bool matches(
const NamedDecl &Node, ast_matchers::internal::ASTMatchFinder *Finder,
ast_matchers::internal::BoundNodesTreeBuilder *Builder) const override {
return llvm::any_of(NameMatchers, [&Node](const NameMatcher &NM) {
return NM.match(Node);
});
}

private:
std::vector<NameMatcher> NameMatchers;
};

Expand Down
4 changes: 4 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ Changes in existing checks
`bsl::optional` and `bdlb::NullableValue` from
<https://github.com/bloomberg/bde>_.

- Improved :doc:`bugprone-unsafe-functions
<clang-tidy/checks/bugprone/unsafe-functions>` check to allow specifying
additional functions to match.

- Improved :doc:`cert-flp30-c <clang-tidy/checks/cert/flp30-c>` check to
fix false positive that floating point variable is only used in increment
expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The check implements the following rules from the CERT C Coding Standard:
Unsafe functions
----------------

The following functions are reported if :option:`ReportDefaultFunctions` is enabled.

If *Annex K.* is available, a replacement from *Annex K.* is suggested for the
following functions:

Expand All @@ -45,8 +47,7 @@ The following functions are always checked, regardless of *Annex K* availability
- ``rewind``, suggested replacement: ``fseek``
- ``setbuf``, suggested replacement: ``setvbuf``

If `ReportMoreUnsafeFunctions
<unsafe-functions.html#cmdoption-arg-ReportMoreUnsafeFunctions>`_ is enabled,
If :option:`ReportMoreUnsafeFunctions` is enabled,
the following functions are also checked:

- ``bcmp``, suggested replacement: ``memcmp``
Expand Down Expand Up @@ -74,6 +75,44 @@ Both macros have to be defined to suggest replacement functions from *Annex K.*
``__STDC_WANT_LIB_EXT1__`` must be defined to ``1`` by the user **before**
including any system headers.

.. _CustomFunctions:

Custom functions
----------------

The option :option:`CustomFunctions` allows the user to define custom functions to be
checked. The format is the following, without newlines:

.. code::

bugprone-unsafe-functions.CustomFunctions="
functionRegex1[, replacement1[, reason1]];
functionRegex2[, replacement2[, reason2]];
...
"

The functions are matched using POSIX extended regular expressions.
*(Note: The regular expressions do not support negative* ``(?!)`` *matches.)*

The `reason` is optional and is used to provide additional information about the
reasoning behind the replacement. The default reason is `is marked as unsafe`.

If `replacement` is empty, the text `it should not be used` will be shown
instead of the suggestion for a replacement.

As an example, the configuration `^original$, replacement, is deprecated;`
will produce the following diagnostic message.

.. code:: c

original(); // warning: function 'original' is deprecated; 'replacement' should be used instead.
::std::original(); // no-warning
original_function(); // no-warning

If the regular expression contains the character `:`, it is matched against the
qualified name (i.e. ``std::original``), otherwise the regex is matched against the unqualified name (``original``).
If the regular expression starts with `::` (or `^::`), it is matched against the
fully qualified name (``::std::original``).

Options
-------
Expand All @@ -86,6 +125,19 @@ Options
this option enables.
Default is `true`.

.. option:: ReportDefaultFunctions

When `true`, the check reports the default set of functions.
Consider changing the setting to false if you only want to see custom
functions matched via :ref:`custom functions<CustomFunctions>`.
Default is `true`.

.. option:: CustomFunctions

A semicolon-separated list of custom functions to be matched. A matched
function contains a regular expression, an optional name of the replacement
function, and an optional reason, separated by comma. For more information,
see :ref:`Custom functions<CustomFunctions>`.

Examples
--------
Expand Down
Loading