https://github.com/dkrupp updated https://github.com/llvm/llvm-project/pull/168691
>From 03433eec012de3ad51e44d0d96721d004ef1c7d7 Mon Sep 17 00:00:00 2001 From: Daniel Krupp <[email protected]> Date: Thu, 30 Oct 2025 13:39:23 +0100 Subject: [PATCH 1/5] [clang-tidy] New bugprone-unsafe-format-string check Adds a new check which warns for the usage of unbounded %s format string specifiers in the sprintf and scanf family functions, which may cause buffer overlfow. --- .../bugprone/BugproneTidyModule.cpp | 3 + .../clang-tidy/bugprone/CMakeLists.txt | 1 + .../bugprone/UnsafeFormatStringCheck.cpp | 149 +++++++++++ .../bugprone/UnsafeFormatStringCheck.h | 34 +++ .../checks/bugprone/unsafe-format-string.rst | 73 ++++++ .../system-header-simulator.h | 57 ++++ .../checkers/bugprone/unsafe-format-string.c | 243 ++++++++++++++++++ .../bugprone/unsafe-format-string.cpp | 58 +++++ 8 files changed, 618 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.c create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp index 6859dc97c112a..15e8a6c3afb6a 100644 --- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp @@ -107,6 +107,7 @@ #include "UnintendedCharOstreamOutputCheck.h" #include "UniquePtrArrayMismatchCheck.h" #include "UnsafeFunctionsCheck.h" +#include "UnsafeFormatStringCheck.h" #include "UnusedLocalNonTrivialVariableCheck.h" #include "UnusedRaiiCheck.h" #include "UnusedReturnValueCheck.h" @@ -308,6 +309,8 @@ class BugproneModule : public ClangTidyModule { "bugprone-crtp-constructor-accessibility"); CheckFactories.registerCheck<UnsafeFunctionsCheck>( "bugprone-unsafe-functions"); + CheckFactories.registerCheck<UnsafeFormatStringCheck>( + "bugprone-unsafe-format-string"); CheckFactories.registerCheck<UnusedLocalNonTrivialVariableCheck>( "bugprone-unused-local-non-trivial-variable"); CheckFactories.registerCheck<UnusedRaiiCheck>("bugprone-unused-raii"); diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt index db1256d91d311..0e9439423ce5a 100644 --- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt @@ -108,6 +108,7 @@ add_clang_library(clangTidyBugproneModule STATIC UnhandledSelfAssignmentCheck.cpp UniquePtrArrayMismatchCheck.cpp UnsafeFunctionsCheck.cpp + UnsafeFormatStringCheck.cpp UnusedLocalNonTrivialVariableCheck.cpp UnusedRaiiCheck.cpp UnusedReturnValueCheck.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp new file mode 100644 index 0000000000000..cd8f6b85ee1e2 --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp @@ -0,0 +1,149 @@ +//===--- UnsafeFormatStringCheck.cpp - clang-tidy -----------------------===// +// +// 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 "clang/ASTMatchers/ASTMatchFinder.h" +#include "llvm/Support/ConvertUTF.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::bugprone { + +UnsafeFormatStringCheck::UnsafeFormatStringCheck(StringRef Name, + ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + +void UnsafeFormatStringCheck::registerMatchers(MatchFinder *Finder) { + // Matches sprintf and scanf family functions in std namespace in C++ and + // globally in C. + auto VulnerableFunctions = + hasAnyName("sprintf", "vsprintf", "scanf", "fscanf", "sscanf", "vscanf", + "vfscanf", "vsscanf", "wscanf", "fwscanf", "swscanf", + "vwscanf", "vfwscanf", "vswscanf"); + Finder->addMatcher( + callExpr(callee(functionDecl(VulnerableFunctions, + anyOf(isInStdNamespace(), + hasParent(translationUnitDecl())))), + anyOf(hasArgument(0, stringLiteral().bind("format")), + hasArgument(1, stringLiteral().bind("format")))) + .bind("call"), + this); +} + +void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { + const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call"); + const auto *Format = Result.Nodes.getNodeAs<StringLiteral>("format"); + + if (!Call || !Format) + return; + + 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); + } + + const auto *Callee = cast<FunctionDecl>(Call->getCalleeDecl()); + StringRef FunctionName = Callee->getName(); + + bool IsScanfFamily = FunctionName.contains("scanf"); + + if (!hasUnboundedStringSpecifier(FormatString, IsScanfFamily)) + return; + + auto Diag = diag(Call->getBeginLoc(), + IsScanfFamily + ? "format specifier '%%s' without field width may cause buffer overflow; consider using '%%Ns' where N limits input length" + : "format specifier '%%s' without precision may cause buffer overflow; consider using '%%.Ns' where N limits output length") + << Call->getSourceRange(); +} + +bool UnsafeFormatStringCheck::hasUnboundedStringSpecifier(StringRef Fmt, + bool IsScanfFamily) { + size_t Pos = 0; + 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; + if (SpecPos < N && Fmt[SpecPos] == '.') { + SpecPos++; + if (SpecPos < N && Fmt[SpecPos] == '*') { + HasPrecision = true; + SpecPos++; + } else { + while (SpecPos < N && isdigit(Fmt[SpecPos])) { + HasPrecision = true; + SpecPos++; + } + } + } + + // 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; +} + +} // namespace clang::tidy::bugprone diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h new file mode 100644 index 0000000000000..b61c7345a0484 --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h @@ -0,0 +1,34 @@ +//===--- UnsafeFormatStringCheck.h - clang-tidy ---------------*- 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" + +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: +/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/unsafe-format-string.html +class UnsafeFormatStringCheck : public ClangTidyCheck { +public: + UnsafeFormatStringCheck(StringRef Name, ClangTidyContext *Context); + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + +private: + bool hasUnboundedStringSpecifier(StringRef FormatString, bool IsScanfFamily); + std::string getSafeAlternative(StringRef FunctionName); +}; + +} // namespace clang::tidy::bugprone + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFORMATSTRINGCHECK_H diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst new file mode 100644 index 0000000000000..9f7ee63fb57f9 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst @@ -0,0 +1,73 @@ +.. title:: clang-tidy - bugprone-unsafe-format-string + +bugprone-unsafe-format-string +============================== + +Detects usage of vulnerable format string functions with unbounded ``%s`` +specifiers that can cause buffer overflows. + +The check identifies calls to format string functions like ``sprintf``, ``scanf``, +and their variants that use ``%s`` format specifiers without proper limits. +This can lead to buffer overflow vulnerabilities when the input string is longer +than the destination buffer. + +Format Specifier Behavior +-------------------------- + +The check distinguishes between different function families: + +**scanf family functions**: Field width limits input length + - ``%s`` - unsafe (no limit) + - ``%99s`` - safe (reads at most 99 characters) + +**sprintf family functions**: Precision limits output length + - ``%s`` - unsafe (no limit) + - ``%99s`` - unsafe (minimum width, no maximum) + - ``%.99s`` - safe (outputs at most 99 characters) + - ``%10.99s`` - safe (minimum 10 chars, maximum 99 chars) + +Examples +-------- + +.. code-block:: c + + char buffer[100]; + const char* input = "user input"; + + // Unsafe sprintf usage + sprintf(buffer, "%s", input); // No limit + sprintf(buffer, "%99s", input); // Field width is minimum, not maximum + + // Safe sprintf usage + sprintf(buffer, "%.99s", input); // Precision limits to 99 chars + sprintf(buffer, "%10.99s", input); // Min 10, max 99 chars + + // Unsafe scanf usage + scanf("%s", buffer); // No limit + + // Safe scanf usage + scanf("%99s", buffer); // Field width limits to 99 chars + + // Safe alternative: use safer functions + snprintf(buffer, sizeof(buffer), "%s", input); + +Checked Functions +----------------- + +The check detects unsafe format strings in these functions: + +**sprintf family** (precision ``.N`` provides safety): +* ``sprintf``, ``vsprintf`` + +**scanf family** (field width ``N`` provides safety): +* ``scanf``, ``fscanf``, ``sscanf`` +* ``vscanf``, ``vfscanf``, ``vsscanf`` +* ``wscanf``, ``fwscanf``, ``swscanf`` +* ``vwscanf``, ``vfwscanf``, ``vswscanf`` + +Recommendations +--------------- + +* For ``sprintf`` family: Use precision specifiers (``%.Ns``) or ``snprintf`` +* For ``scanf`` family: Use field width specifiers (``%Ns``) +* Consider using safer string handling functions when possible diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h new file mode 100644 index 0000000000000..fef2c10ae339a --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h @@ -0,0 +1,57 @@ +#pragma clang system_header + +#ifdef __cplusplus +#define restrict /*restrict*/ +#endif + +#ifndef __cplusplus +typedef __WCHAR_TYPE__ wchar_t; +#endif + +typedef __typeof(sizeof(int)) size_t; +typedef long long __int64_t; +typedef __int64_t __darwin_off_t; +typedef __darwin_off_t fpos_t; +typedef int off_t; +typedef long ssize_t; + +typedef struct _FILE FILE; + +extern FILE *stdin; +extern FILE *stdout; +extern FILE *stderr; + +typedef __builtin_va_list va_list; +#define va_start(ap, param) __builtin_va_start(ap, param) +#define va_end(ap) __builtin_va_end(ap) +#define va_arg(ap, type) __builtin_va_arg(ap, type) +#define va_copy(dst, src) __builtin_va_copy(dst, src) + + +#ifdef __cplusplus +namespace std { +#endif +extern int fscanf ( FILE *restrict stream, const char *restrict format, ... ); +extern int scanf ( const char *restrict format, ... ); +extern int sscanf ( const char *restrict s, const char *restrict format, ...); +extern int vscanf( const char *restrict format, va_list vlist ); +extern int vfscanf ( FILE *restrict stream, const char *restrict format, va_list arg ); + +extern int vsscanf( const char *restrict buffer, const char *restrict format, va_list vlist ); +extern int vwscanf( const wchar_t* format, va_list vlist ); +extern int vfwscanf( FILE* stream, const wchar_t* format, va_list vlist ); +extern int vswscanf( const wchar_t* buffer, const wchar_t* format, va_list vlist ); +extern int swscanf (const wchar_t* ws, const wchar_t* format, ...); +extern int wscanf( const wchar_t *format, ... ); +extern int fwscanf( FILE *stream, const wchar_t *format, ... ); + +extern int printf( const char* format, ... ); +extern int sprintf( char* buffer, const char* format, ... ); +extern int vsprintf (char * s, const char * format, va_list arg ); +extern int vsnprintf (char * s, size_t n, const char * format, va_list arg ); +extern int fprintf( FILE* stream, const char* format, ... ); +extern int snprintf( char* restrict buffer, size_t bufsz, + const char* restrict format, ... ); +#ifdef __cplusplus +} //namespace std { +#endif \ No newline at end of file diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.c b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.c new file mode 100644 index 0000000000000..2e3077ecab35a --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.c @@ -0,0 +1,243 @@ +// RUN: %check_clang_tidy %s bugprone-unsafe-format-string %t -- -- -isystem %S/Inputs/unsafe-format-string + +#include <system-header-simulator.h> + +void test_sprintf() { + char buffer[100]; + const char* input = "user input"; + + /* Positive: unsafe %s without field width */ + sprintf(buffer, "%s", input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: field width doesn't prevent overflow in sprintf */ + sprintf(buffer, "%99s", input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: dynamic field width doesn't prevent overflow */ + sprintf(buffer, "%*s", 10, input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /*Negative: precision limits string length */ + sprintf(buffer, "%.99s", input); + /* no-warning */ + + /*Negative: precision with field width */ + sprintf(buffer, "%1.99s", input); + /* no-warning */ + + /*Negative: dynamic precision */ + sprintf(buffer, "%.*s", 99, input); + /* no-warning */ + + /*Negative: field width with dynamic precision */ + sprintf(buffer, "%1.*s", 99, input); + /* no-warning */ + + /*Negative: dynamic field width with fixed precision */ + sprintf(buffer, "%*.99s", 10, input); + /* no-warning */ + + /*Negative: dynamic field width and precision */ + sprintf(buffer, "%*.*s", 10, 99, input); + /* no-warning */ + + /*Negative: other format specifiers are safe */ + sprintf(buffer, "%d %f", 42, 3.14); + /* no-warning */ +} + +void test_vsprintf() { + char buffer[100]; + va_list args; + + /* Positive: unsafe %s without field width */ + vsprintf(buffer, "%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: field width doesn't prevent overflow in vsprintf */ + vsprintf(buffer, "%99s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: precision limits string length */ + vsprintf(buffer, "%.99s", args); + /* no-warning */ +} + +void test_vsnprintf(int count, ...) { + va_list args; + va_start(args, count); + char buffer[100]; + + /*Negative: vsnprintf is safe */ + vsnprintf(buffer, sizeof(buffer), "%99s", args); + /* no-warning */ + + va_end(args); +} + +void test_scanf() { + char buffer[100]; + + /* Positive: unsafe %s without field width */ + scanf("%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + scanf("%99s", buffer); + /* no-warning */ +} + +void test_fscanf() { + char buffer[100]; + FILE* file = 0; + + /* Positive: unsafe %s without field width */ + fscanf(file, "%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + fscanf(file, "%99s", buffer); + /* no-warning */ +} + +void test_sscanf(char *source) { + char buffer[100]; + + /* Positive: unsafe %s without field width */ + sscanf(source, "%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + sscanf(source, "%99s", buffer); + /* no-warning */ +} + +void test_vfscanf() { + FILE* file = 0; + va_list args; + + /* Positive: unsafe %s without field width */ + vfscanf(file, "%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vfscanf(file, "%99s", args); + /* no-warning */ +} + +void test_vsscanf(char * source) { + va_list args; + + /* Positive: unsafe %s without field width */ + vsscanf(source, "%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vsscanf(source, "%99s", args); + /* no-warning */ +} + +void test_vscanf() { + va_list args; + + /* Positive: unsafe %s without field width */ + vscanf("%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vscanf("%99s", args); + /* no-warning */ +} + +void test_wscanf() { + wchar_t buffer[100]; + + /* Positive: unsafe %s without field width */ + wscanf(L"%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + wscanf(L"%99s", buffer); + /* no-warning */ +} + +void test_fwscanf() { + wchar_t buffer[100]; + FILE* file = 0; + + /* Positive: unsafe %s without field width */ + fwscanf(file, L"%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + fwscanf(file, L"%99s", buffer); + /* no-warning */ +} + +void test_swscanf(wchar_t *source) { + wchar_t buffer[100]; + + /* Positive: unsafe %s without field width */ + swscanf(source, L"%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + swscanf(source, L"%99s", buffer); + /* no-warning */ +} + +void test_vwscanf() { + va_list args; + + /* Positive: unsafe %s without field width */ + vwscanf(L"%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vwscanf(L"%99s", args); + /* no-warning */ +} + +void test_vfwscanf() { + FILE* file = 0; + va_list args; + + /* Positive: unsafe %s without field width */ + vfwscanf(file, L"%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vfwscanf(file, L"%99s", args); + /* no-warning */ +} + +void test_vswscanf() { + const wchar_t* source = L"input"; + va_list args; + + /* Positive: unsafe %s without field width */ + vswscanf(source, L"%s", args); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + vswscanf(source, L"%99s", args); + /* no-warning */ +} + +void test_safe_alternatives() { + char buffer[100]; + const char* input = "user input"; + + /*Negative: snprintf is inherently safe */ + snprintf(buffer, sizeof(buffer), "%s", input); + /* no-warning */ + + /*Negative: printf family doesn't write to buffers */ + printf("%s", input); + /* no-warning */ + + /*Negative: fprintf doesn't write to user buffers */ + fprintf(stderr, "%s", input); + /* no-warning */ +} diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.cpp new file mode 100644 index 0000000000000..07117e37b7521 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string.cpp @@ -0,0 +1,58 @@ +// RUN: %check_clang_tidy %s bugprone-unsafe-format-string %t -- -- -isystem %S/Inputs/unsafe-format-string + +#include <system-header-simulator.h> + +class TestClass{ + int sprintf( char* buffer, const char* format, ... ); + void test_sprintf() { + char buffer[100]; + const char* input = "user input"; + /* Negative: no warning for calling member functions */ + sprintf(buffer, "%s", input); + } +}; + +void test_sprintf() { + char buffer[100]; + const char* input = "user input"; + + /* Positive: unsafe %s without field width */ + std::sprintf(buffer, "%s", input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: field width doesn't prevent overflow in sprintf */ + std::sprintf(buffer, "%99s", input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /* Positive: dynamic field width doesn't prevent overflow */ + std::sprintf(buffer, "%*s", 10, input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + /*Negative: precision limits string length */ + std::sprintf(buffer, "%.99s", input); + /* no-warning */ + + /*Negative: precision with field width */ + std::sprintf(buffer, "%1.99s", input); + /* no-warning */ + + /*Negative: dynamic precision */ + std::sprintf(buffer, "%.*s", 99, input); + /* no-warning */ + + /*Negative: field width with dynamic precision */ + std::sprintf(buffer, "%1.*s", 99, input); + /* no-warning */ + + /*Negative: dynamic field width with fixed precision */ + std::sprintf(buffer, "%*.99s", 10, input); + /* no-warning */ + + /*Negative: dynamic field width and precision */ + std::sprintf(buffer, "%*.*s", 10, 99, input); + /* no-warning */ + + /*Negative: other format specifiers are safe */ + std::sprintf(buffer, "%d %f", 42, 3.14); + /* no-warning */ +} >From cefe2742bab0507b774abf635eb3679cf7fc235d Mon Sep 17 00:00:00 2001 From: Daniel Krupp <[email protected]> Date: Wed, 19 Nov 2025 12:04:37 +0100 Subject: [PATCH 2/5] Fixup formmatting --- .../clang-tidy/bugprone/BugproneTidyModule.cpp | 2 +- .../bugprone/UnsafeFormatStringCheck.cpp | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp index 15e8a6c3afb6a..940d13bf60a18 100644 --- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp @@ -106,8 +106,8 @@ #include "UnhandledSelfAssignmentCheck.h" #include "UnintendedCharOstreamOutputCheck.h" #include "UniquePtrArrayMismatchCheck.h" -#include "UnsafeFunctionsCheck.h" #include "UnsafeFormatStringCheck.h" +#include "UnsafeFunctionsCheck.h" #include "UnusedLocalNonTrivialVariableCheck.h" #include "UnusedRaiiCheck.h" #include "UnusedReturnValueCheck.h" diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp index cd8f6b85ee1e2..48a05aa27076c 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp @@ -61,11 +61,15 @@ void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { if (!hasUnboundedStringSpecifier(FormatString, IsScanfFamily)) return; - auto Diag = diag(Call->getBeginLoc(), - IsScanfFamily - ? "format specifier '%%s' without field width may cause buffer overflow; consider using '%%Ns' where N limits input length" - : "format specifier '%%s' without precision may cause buffer overflow; consider using '%%.Ns' where N limits output length") - << Call->getSourceRange(); + auto Diag = + diag( + Call->getBeginLoc(), + IsScanfFamily + ? "format specifier '%%s' without field width may cause buffer " + "overflow; consider using '%%Ns' where N limits input length" + : "format specifier '%%s' without precision may cause buffer " + "overflow; consider using '%%.Ns' where N limits output length") + << Call->getSourceRange(); } bool UnsafeFormatStringCheck::hasUnboundedStringSpecifier(StringRef Fmt, >From c5b8b1b2f186e86c7284540067c33d88694b4b45 Mon Sep 17 00:00:00 2001 From: Daniel Krupp <[email protected]> Date: Wed, 19 Nov 2025 12:14:41 +0100 Subject: [PATCH 3/5] Fixup. Fixing clang-tidy finding --- clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h index b61c7345a0484..829a0858b0dcd 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h @@ -25,7 +25,7 @@ class UnsafeFormatStringCheck : public ClangTidyCheck { void check(const ast_matchers::MatchFinder::MatchResult &Result) override; private: - bool hasUnboundedStringSpecifier(StringRef FormatString, bool IsScanfFamily); + bool hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily); std::string getSafeAlternative(StringRef FunctionName); }; >From e88df90cd67392f3ea4ce591df5a88913ed8554e Mon Sep 17 00:00:00 2001 From: Daniel Krupp <[email protected]> Date: Wed, 26 Nov 2025 20:10:57 +0100 Subject: [PATCH 4/5] Review fixes --- .../bugprone/BugproneTidyModule.cpp | 4 +-- .../clang-tidy/bugprone/CMakeLists.txt | 2 +- .../bugprone/UnsafeFormatStringCheck.cpp | 34 +++++++++---------- .../bugprone/UnsafeFormatStringCheck.h | 4 +-- .../checks/bugprone/unsafe-format-string.rst | 4 +-- .../system-header-simulator.h | 2 +- 6 files changed, 24 insertions(+), 26 deletions(-) diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp index 940d13bf60a18..367504623cee6 100644 --- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp @@ -307,10 +307,10 @@ class BugproneModule : public ClangTidyModule { "bugprone-unique-ptr-array-mismatch"); CheckFactories.registerCheck<CrtpConstructorAccessibilityCheck>( "bugprone-crtp-constructor-accessibility"); - CheckFactories.registerCheck<UnsafeFunctionsCheck>( - "bugprone-unsafe-functions"); CheckFactories.registerCheck<UnsafeFormatStringCheck>( "bugprone-unsafe-format-string"); + CheckFactories.registerCheck<UnsafeFunctionsCheck>( + "bugprone-unsafe-functions"); CheckFactories.registerCheck<UnusedLocalNonTrivialVariableCheck>( "bugprone-unused-local-non-trivial-variable"); CheckFactories.registerCheck<UnusedRaiiCheck>("bugprone-unused-raii"); diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt index 0e9439423ce5a..88b776d9239d8 100644 --- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt @@ -107,8 +107,8 @@ add_clang_library(clangTidyBugproneModule STATIC UnhandledExceptionAtNewCheck.cpp UnhandledSelfAssignmentCheck.cpp UniquePtrArrayMismatchCheck.cpp - UnsafeFunctionsCheck.cpp UnsafeFormatStringCheck.cpp + UnsafeFunctionsCheck.cpp UnusedLocalNonTrivialVariableCheck.cpp UnusedRaiiCheck.cpp UnusedReturnValueCheck.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp index 48a05aa27076c..72c34589956d8 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp @@ -1,4 +1,4 @@ -//===--- UnsafeFormatStringCheck.cpp - clang-tidy -----------------------===// +//===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. @@ -26,11 +26,12 @@ void UnsafeFormatStringCheck::registerMatchers(MatchFinder *Finder) { "vfscanf", "vsscanf", "wscanf", "fwscanf", "swscanf", "vwscanf", "vfwscanf", "vswscanf"); Finder->addMatcher( - callExpr(callee(functionDecl(VulnerableFunctions, - anyOf(isInStdNamespace(), - hasParent(translationUnitDecl())))), - anyOf(hasArgument(0, stringLiteral().bind("format")), - hasArgument(1, stringLiteral().bind("format")))) + callExpr( + callee(functionDecl(VulnerableFunctions, + anyOf(isInStdNamespace(), + hasDeclContext(translationUnitDecl())))), + anyOf(hasArgument(0, stringLiteral().bind("format")), + hasArgument(1, stringLiteral().bind("format")))) .bind("call"), this); } @@ -39,8 +40,7 @@ void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call"); const auto *Format = Result.Nodes.getNodeAs<StringLiteral>("format"); - if (!Call || !Format) - return; + assert(Call && Format); std::string FormatString; if (Format->getCharByteWidth() == 1) { @@ -54,21 +54,19 @@ void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { } const auto *Callee = cast<FunctionDecl>(Call->getCalleeDecl()); - StringRef FunctionName = Callee->getName(); + const StringRef FunctionName = Callee->getName(); - bool IsScanfFamily = FunctionName.contains("scanf"); + const bool IsScanfFamily = FunctionName.contains("scanf"); if (!hasUnboundedStringSpecifier(FormatString, IsScanfFamily)) return; - auto Diag = - diag( - Call->getBeginLoc(), - IsScanfFamily - ? "format specifier '%%s' without field width may cause buffer " - "overflow; consider using '%%Ns' where N limits input length" - : "format specifier '%%s' without precision may cause buffer " - "overflow; consider using '%%.Ns' where N limits output length") + diag(Call->getBeginLoc(), + IsScanfFamily + ? "format specifier '%%s' without field width may cause buffer " + "overflow; consider using '%%Ns' where N limits input length" + : "format specifier '%%s' without precision may cause buffer " + "overflow; consider using '%%.Ns' where N limits output length") << Call->getSourceRange(); } diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h index 829a0858b0dcd..169ffcf13be8c 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h @@ -1,4 +1,4 @@ -//===--- UnsafeFormatStringCheck.h - clang-tidy ---------------*- C++ -*-===// +//===------------------------------------------------------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. @@ -17,7 +17,7 @@ namespace clang::tidy::bugprone { /// specifiers that can cause buffer overflows. /// /// For the user-facing documentation see: -/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/unsafe-format-string.html +/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/unsafe-format-string.html class UnsafeFormatStringCheck : public ClangTidyCheck { public: UnsafeFormatStringCheck(StringRef Name, ClangTidyContext *Context); diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst index 9f7ee63fb57f9..e9724d9709bdf 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst @@ -1,7 +1,7 @@ .. title:: clang-tidy - bugprone-unsafe-format-string bugprone-unsafe-format-string -============================== +============================= Detects usage of vulnerable format string functions with unbounded ``%s`` specifiers that can cause buffer overflows. @@ -12,7 +12,7 @@ This can lead to buffer overflow vulnerabilities when the input string is longer than the destination buffer. Format Specifier Behavior --------------------------- +------------------------- The check distinguishes between different function families: diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h index fef2c10ae339a..6cc0bf799a930 100644 --- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/Inputs/unsafe-format-string/system-header-simulator.h @@ -54,4 +54,4 @@ extern int snprintf( char* restrict buffer, size_t bufsz, const char* restrict format, ... ); #ifdef __cplusplus } //namespace std { -#endif \ No newline at end of file +#endif >From 272e66e5f78afc978eb7dce831a05cca52ad59ac Mon Sep 17 00:00:00 2001 From: Daniel Krupp <[email protected]> Date: Tue, 2 Dec 2025 22:10:34 +0100 Subject: [PATCH 5/5] Add configuration options to the checker CustomPrintfFunctions and CustomScanfFunctions are added to the checker where the user can define custom dangerous scanf/printf like functions with the function_name, stringliteralcount parameter pairs. --- .../bugprone/UnsafeFormatStringCheck.cpp | 136 ++++++++++++++++-- .../bugprone/UnsafeFormatStringCheck.h | 13 ++ .../checks/bugprone/unsafe-format-string.rst | 38 +++++ .../docs/clang-tidy/checks/list.rst | 1 + .../unsafe-format-string-custom-config.c | 33 +++++ 5 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string-custom-config.c diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp index 72c34589956d8..ffa6f2bb4cd54 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.cpp @@ -7,16 +7,61 @@ //===----------------------------------------------------------------------===// #include "UnsafeFormatStringCheck.h" +#include "../utils/OptionsUtils.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include "llvm/Support/ConvertUTF.h" +#include <string> using namespace clang::ast_matchers; namespace clang::tidy::bugprone { +static constexpr llvm::StringLiteral OptionNameCustomPrintfFunctions = + "CustomPrintfFunctions"; +static constexpr llvm::StringLiteral OptionNameCustomScanfFunctions = + "CustomScanfFunctions"; + +static constexpr llvm::StringLiteral BuiltInFormatBind = "format"; +static constexpr llvm::StringLiteral BuiltInCallBind = "call"; +static constexpr llvm::StringLiteral PrintfCallBind = "printfcall"; +static constexpr llvm::StringLiteral ScanfCallBind = "scanfcall"; + +static std::vector<UnsafeFormatStringCheck::CheckedFunction> +parseCheckedFunctions(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.") + << OptionNameCustomPrintfFunctions; + continue; + } + Result.push_back( + {Name.trim().str(), + matchers::MatchesAnyListedNameMatcher::NameMatcher(Name.trim()), + Count}); + } + + return Result; +} + UnsafeFormatStringCheck::UnsafeFormatStringCheck(StringRef Name, ClangTidyContext *Context) - : ClangTidyCheck(Name, Context) {} + : ClangTidyCheck(Name, Context), + CustomPrintfFunctions(parseCheckedFunctions( + Options.get(OptionNameCustomPrintfFunctions, ""), Context)), + CustomScanfFunctions(parseCheckedFunctions( + Options.get(OptionNameCustomScanfFunctions, ""), Context)) {} void UnsafeFormatStringCheck::registerMatchers(MatchFinder *Finder) { // Matches sprintf and scanf family functions in std namespace in C++ and @@ -30,17 +75,87 @@ void UnsafeFormatStringCheck::registerMatchers(MatchFinder *Finder) { callee(functionDecl(VulnerableFunctions, anyOf(isInStdNamespace(), hasDeclContext(translationUnitDecl())))), - anyOf(hasArgument(0, stringLiteral().bind("format")), - hasArgument(1, stringLiteral().bind("format")))) - .bind("call"), + anyOf(hasArgument(0, stringLiteral().bind(BuiltInFormatBind)), + hasArgument(1, stringLiteral().bind(BuiltInFormatBind)))) + .bind(BuiltInCallBind), this); + + if (!CustomPrintfFunctions.empty()) { + std::vector<llvm::StringRef> FunctionNames; + FunctionNames.reserve(CustomPrintfFunctions.size()); + + for (const auto &Entry : CustomPrintfFunctions) + FunctionNames.emplace_back(Entry.Name); + + auto CustomFunctionsMatcher = matchers::matchesAnyListedName(FunctionNames); + + Finder->addMatcher(callExpr(callee((functionDecl(CustomFunctionsMatcher)))) + .bind(PrintfCallBind), + this); + } + + if (!CustomScanfFunctions.empty()) { + std::vector<llvm::StringRef> FunctionNames; + FunctionNames.reserve(CustomScanfFunctions.size()); + + for (const auto &Entry : CustomScanfFunctions) + FunctionNames.emplace_back(Entry.Name); + + auto CustomFunctionsMatcher = matchers::matchesAnyListedName(FunctionNames); + + Finder->addMatcher(callExpr(callee((functionDecl(CustomFunctionsMatcher)))) + .bind(ScanfCallBind), + this); + } +} + +void UnsafeFormatStringCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, OptionNameCustomPrintfFunctions, ""); + Options.store(Opts, OptionNameCustomScanfFunctions, ""); +} + +const StringLiteral *UnsafeFormatStringCheck::getFormatLiteral( + const CallExpr *Call, const std::vector<CheckedFunction> &CustomFunctions) { + const FunctionDecl *FD = cast<FunctionDecl>(Call->getDirectCallee()); + if (!FD) + return nullptr; + for (const auto &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); + } + } + return nullptr; } void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { - const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call"); - const auto *Format = Result.Nodes.getNodeAs<StringLiteral>("format"); + const CallExpr *Call; + const StringLiteral *Format; + bool IsScanfFamily = false; + if (Result.Nodes.getNodeAs<CallExpr>(BuiltInCallBind)) { + Call = Result.Nodes.getNodeAs<CallExpr>(BuiltInCallBind); + Format = Result.Nodes.getNodeAs<StringLiteral>(BuiltInFormatBind); + const auto *Callee = cast<FunctionDecl>(Call->getCalleeDecl()); + const StringRef FunctionName = Callee->getName(); + IsScanfFamily = FunctionName.contains("scanf"); + } else if (Result.Nodes.getNodeAs<CallExpr>(PrintfCallBind)) { + Call = Result.Nodes.getNodeAs<CallExpr>(PrintfCallBind); + Format = getFormatLiteral(Call, CustomPrintfFunctions); + IsScanfFamily = false; + } else if (Result.Nodes.getNodeAs<CallExpr>(ScanfCallBind)) { + Call = Result.Nodes.getNodeAs<CallExpr>(ScanfCallBind); + Format = getFormatLiteral(Call, CustomScanfFunctions); + IsScanfFamily = true; + } else { + Call = nullptr; + Format = nullptr; + } - assert(Call && Format); + if (!Call || !Format) + return; std::string FormatString; if (Format->getCharByteWidth() == 1) { @@ -53,11 +168,6 @@ void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { convertUTF32ToUTF8String(Format->getBytes(), FormatString); } - const auto *Callee = cast<FunctionDecl>(Call->getCalleeDecl()); - const StringRef FunctionName = Callee->getName(); - - const bool IsScanfFamily = FunctionName.contains("scanf"); - if (!hasUnboundedStringSpecifier(FormatString, IsScanfFamily)) return; @@ -73,7 +183,7 @@ void UnsafeFormatStringCheck::check(const MatchFinder::MatchResult &Result) { bool UnsafeFormatStringCheck::hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily) { size_t Pos = 0; - size_t N = Fmt.size(); + const size_t N = Fmt.size(); while ((Pos = Fmt.find('%', Pos)) != StringRef::npos) { if (Pos + 1 >= N) break; diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h index 169ffcf13be8c..98ada92b3ae36 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h +++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h @@ -10,6 +10,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_UNSAFEFORMATSTRINGCHECK_H #include "../ClangTidyCheck.h" +#include "../utils/Matchers.h" namespace clang::tidy::bugprone { @@ -21,10 +22,22 @@ namespace clang::tidy::bugprone { 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::MatchesAnyListedNameMatcher::NameMatcher Pattern; + unsigned long FormatStringLocation; + }; + private: + const std::vector<CheckedFunction> CustomPrintfFunctions; + const std::vector<CheckedFunction> CustomScanfFunctions; + const StringLiteral * + getFormatLiteral(const CallExpr *, + const std::vector<CheckedFunction> &CustomFunctions); bool hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily); std::string getSafeAlternative(StringRef FunctionName); }; diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst index e9724d9709bdf..c0b8c0c5f06e8 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/unsafe-format-string.rst @@ -51,6 +51,7 @@ Examples // Safe alternative: use safer functions snprintf(buffer, sizeof(buffer), "%s", input); + Checked Functions ----------------- @@ -65,6 +66,43 @@ The check detects unsafe format strings in these functions: * ``wscanf``, ``fwscanf``, ``swscanf`` * ``vwscanf``, ``vfwscanf``, ``vswscanf`` +Configuration +------------- + +The checker offers 2 configuration options. + +* `CustomPrintfFunctions` The user can specify own printf-like functions with dangerous format string parameter. +* `CustomScanfFunctions` The user can specify own scanf-like functions with dangerous format string parameter. + +Format: +Both options have the following format. +.. code:: + + bugprone-unsafe-functions.CustomPrintfFunctions=" + functionRegex1, format-string-position; + functionRegex2, format-string-position; + ... + " + +The first parameter in the pairs is a function regular expression matching the function name, +the second parameter is the count of the format string literal argument. + +The following configuration will give warning: + +.. code:: + + bugprone-unsafe-format-string.CustomPrintfFunctions: 'mysprintf, 0;' + bugprone-unsafe-format-string.CustomScanfFunctions: 'myscanf, 1;' + + extern int myscanf( const char* format, ... ); + extern int mysprintf( char* buffer, const char* format, ... ); + void test() { + char buffer[100]; + const char* input = "user input"; + mysprintf(buffer, "%s", input); // warning + myscanf("%s", buffer); //warning + } + Recommendations --------------- diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 8bb112f3d1832..f9a59694243c2 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -174,6 +174,7 @@ Clang-Tidy Checks :doc:`bugprone-unhandled-self-assignment <bugprone/unhandled-self-assignment>`, :doc:`bugprone-unintended-char-ostream-output <bugprone/unintended-char-ostream-output>`, "Yes" :doc:`bugprone-unique-ptr-array-mismatch <bugprone/unique-ptr-array-mismatch>`, "Yes" + :doc:`bugprone-unsafe-format-string <bugprone/unsafe-format-string>`, :doc:`bugprone-unsafe-functions <bugprone/unsafe-functions>`, :doc:`bugprone-unused-local-non-trivial-variable <bugprone/unused-local-non-trivial-variable>`, :doc:`bugprone-unused-raii <bugprone/unused-raii>`, "Yes" diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string-custom-config.c b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string-custom-config.c new file mode 100644 index 0000000000000..d817ac384fd37 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/unsafe-format-string-custom-config.c @@ -0,0 +1,33 @@ +// RUN: %check_clang_tidy %s bugprone-unsafe-format-string %t --\ +// RUN: -config="{CheckOptions: {bugprone-unsafe-format-string.CustomPrintfFunctions: 'mysprintf, 1;', bugprone-unsafe-format-string.CustomScanfFunctions: 'myscanf, 0;' }}"\ +// RUN: -- -isystem %S/Inputs/unsafe-format-string + +#include <system-header-simulator.h> + +extern int myscanf( const char* format, ... ); +extern int mysprintf( char* buffer, const char* format, ... ); + +void test_sprintf() { + char buffer[100]; + const char* input = "user input"; + + /* Positive: unsafe %s without field width */ + mysprintf(buffer, "%s", input); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without precision may cause buffer overflow; consider using '%.Ns' where N limits output length [bugprone-unsafe-format-string] + + mysprintf(buffer, "%.99s", input); + /*no warning*/ +} + + +void test_scanf() { + char buffer[100]; + + /* Positive: unsafe %s without field width */ + myscanf("%s", buffer); + // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: format specifier '%s' without field width may cause buffer overflow; consider using '%Ns' where N limits input length [bugprone-unsafe-format-string] + + /*Negative: safe %s with field width */ + myscanf("%99s", buffer); + /* no-warning */ +} \ No newline at end of file _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
