[clang-tools-extra] [clang-tidy] New bugprone-unsafe-format-string check (PR #168691)
Daniel Krupp via cfe-commits
cfe-commits at lists.llvm.org
Tue Dec 2 13:21:28 PST 2025
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 <daniel.krupp at ericsson.com>
Date: Thu, 30 Oct 2025 13:39:23 +0100
Subject: [PATCH 1/6] [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 <daniel.krupp at ericsson.com>
Date: Wed, 19 Nov 2025 12:04:37 +0100
Subject: [PATCH 2/6] 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 <daniel.krupp at ericsson.com>
Date: Wed, 19 Nov 2025 12:14:41 +0100
Subject: [PATCH 3/6] 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 <daniel.krupp at ericsson.com>
Date: Wed, 26 Nov 2025 20:10:57 +0100
Subject: [PATCH 4/6] 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 <daniel.krupp at ericsson.com>
Date: Tue, 2 Dec 2025 22:10:34 +0100
Subject: [PATCH 5/6] 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
>From 6fa63afcf82f0c99c58da9abf73308993f825f98 Mon Sep 17 00:00:00 2001
From: Daniel Krupp <daniel.krupp at ericsson.com>
Date: Tue, 2 Dec 2025 22:21:08 +0100
Subject: [PATCH 6/6] fixup
---
.../clang-tidy/bugprone/UnsafeFormatStringCheck.h | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h
index 98ada92b3ae36..e648b710421cd 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h
+++ b/clang-tools-extra/clang-tidy/bugprone/UnsafeFormatStringCheck.h
@@ -38,8 +38,7 @@ class UnsafeFormatStringCheck : public ClangTidyCheck {
const StringLiteral *
getFormatLiteral(const CallExpr *,
const std::vector<CheckedFunction> &CustomFunctions);
- bool hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily);
- std::string getSafeAlternative(StringRef FunctionName);
+ bool hasUnboundedStringSpecifier(StringRef Fmt, bool IsScanfFamily);
};
} // namespace clang::tidy::bugprone
More information about the cfe-commits
mailing list