https://github.com/voyager-jhk updated https://github.com/llvm/llvm-project/pull/203757
>From c720d1b4ba9f04b20be268db7f0b985f061f0c44 Mon Sep 17 00:00:00 2001 From: SiHuaN <[email protected]> Date: Wed, 10 Jun 2026 15:20:16 +0800 Subject: [PATCH] [clang-tidy] Add bugprone-lambda-capture-lifetime check This patch introduces a new check to detect lambdas that capture local variables by reference and subsequently escape the local scope, leading to use-after-free. It identifies two primary escape mechanisms: 1. Concurrency sinks: Passed to asynchronous execution APIs (e.g., std::thread). 2. Storage sinks: Stored in containers (e.g., std::vector) that have global or field storage duration. The AST matcher explicitly unwraps the argument conversion spine to accurately target the escaping lambdas. All escape sinks are configurable via CheckOptions. --- .../bugprone/BugproneTidyModule.cpp | 3 + .../clang-tidy/bugprone/CMakeLists.txt | 1 + .../bugprone/LambdaCaptureLifetimeCheck.cpp | 151 ++++++++++++++++++ .../bugprone/LambdaCaptureLifetimeCheck.h | 42 +++++ clang-tools-extra/docs/ReleaseNotes.rst | 6 + .../bugprone/lambda-capture-lifetime.rst | 59 +++++++ .../docs/clang-tidy/checks/list.rst | 1 + .../bugprone/lambda-capture-lifetime.cpp | 66 ++++++++ 8 files changed, 329 insertions(+) create mode 100644 clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.cpp create mode 100644 clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.h create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/lambda-capture-lifetime.rst create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/lambda-capture-lifetime.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp index 3aa39d10ceb5d..f10ec5dae5bbb 100644 --- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp @@ -45,6 +45,7 @@ #include "InfiniteLoopCheck.h" #include "IntegerDivisionCheck.h" #include "InvalidEnumDefaultInitializationCheck.h" +#include "LambdaCaptureLifetimeCheck.h" #include "LambdaFunctionNameCheck.h" #include "MacroParenthesesCheck.h" #include "MacroRepeatedSideEffectsCheck.h" @@ -184,6 +185,8 @@ class BugproneModule : public ClangTidyModule { "bugprone-incorrect-enable-if"); CheckFactories.registerCheck<IncorrectEnableSharedFromThisCheck>( "bugprone-incorrect-enable-shared-from-this"); + CheckFactories.registerCheck<LambdaCaptureLifetimeCheck>( + "bugprone-lambda-capture-lifetime"); CheckFactories.registerCheck<UnintendedCharOstreamOutputCheck>( "bugprone-unintended-char-ostream-output"); CheckFactories.registerCheck<ReturnConstRefFromParameterCheck>( diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt index 43e85b1407f21..33a07e3243aa0 100644 --- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt @@ -38,6 +38,7 @@ add_clang_library(clangTidyBugproneModule STATIC IncorrectEnableIfCheck.cpp IncorrectEnableSharedFromThisCheck.cpp InvalidEnumDefaultInitializationCheck.cpp + LambdaCaptureLifetimeCheck.cpp MissingEndComparisonCheck.cpp UnintendedCharOstreamOutputCheck.cpp ReturnConstRefFromParameterCheck.cpp diff --git a/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.cpp new file mode 100644 index 0000000000000..ce4de0d5b9538 --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.cpp @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// 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 "LambdaCaptureLifetimeCheck.h" +#include "../utils/OptionsUtils.h" +#include "clang/AST/ASTContext.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" + +using namespace clang::ast_matchers; + +namespace clang::tidy::bugprone { +namespace { + +static const LambdaExpr *getEscapingLambdaFromArgument(const Expr *E) { + if (!E) + return nullptr; + + E = E->IgnoreParenImpCasts(); + + if (const auto *Lambda = dyn_cast<LambdaExpr>(E)) + return Lambda; + + if (const auto *Cleanups = dyn_cast<ExprWithCleanups>(E)) + return getEscapingLambdaFromArgument(Cleanups->getSubExpr()); + + if (const auto *Temporary = dyn_cast<MaterializeTemporaryExpr>(E)) + return getEscapingLambdaFromArgument(Temporary->getSubExpr()); + + if (const auto *Temporary = dyn_cast<CXXBindTemporaryExpr>(E)) + return getEscapingLambdaFromArgument(Temporary->getSubExpr()); + + if (const auto *Cast = dyn_cast<CastExpr>(E)) + return getEscapingLambdaFromArgument(Cast->getSubExpr()); + + if (const auto *Construct = dyn_cast<CXXConstructExpr>(E)) { + if (Construct->getNumArgs() == 1) + return getEscapingLambdaFromArgument(Construct->getArg(0)); + return nullptr; + } + + if (const auto *InitList = dyn_cast<InitListExpr>(E)) { + if (InitList->getNumInits() == 1) + return getEscapingLambdaFromArgument(InitList->getInit(0)); + return nullptr; + } + + return nullptr; +} + +static const LambdaExpr *getEscapingLambda(const CXXConstructExpr *Construct) { + for (const Expr *Arg : Construct->arguments()) + if (const LambdaExpr *Lambda = getEscapingLambdaFromArgument(Arg)) + return Lambda; + return nullptr; +} + +static const LambdaExpr *getEscapingLambda(const CallExpr *Call) { + for (const Expr *Arg : Call->arguments()) + if (const LambdaExpr *Lambda = getEscapingLambdaFromArgument(Arg)) + return Lambda; + return nullptr; +} + +static bool capturesLocalVariableByReference(const LambdaExpr *Lambda) { + return llvm::any_of(Lambda->captures(), [](const LambdaCapture &Capture) { + if (Capture.capturesVariable() && Capture.getCaptureKind() == LCK_ByRef) { + if (const auto *Var = dyn_cast<VarDecl>(Capture.getCapturedVar())) { + return Var->hasLocalStorage(); + } + } + return false; + }); +} + +} // namespace + +LambdaCaptureLifetimeCheck::LambdaCaptureLifetimeCheck( + StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + AsyncClasses(utils::options::parseStringList( + Options.get("AsyncClasses", "::std::thread;::std::jthread"))), + AsyncFunctions(utils::options::parseStringList( + Options.get("AsyncFunctions", "::std::async"))), + StorageClasses(utils::options::parseStringList( + Options.get("StorageClasses", "::std::vector"))), + StorageFunctions(utils::options::parseStringList(Options.get( + "StorageFunctions", "push_back;emplace_back;insert;assign"))) {} + +void LambdaCaptureLifetimeCheck::storeOptions( + ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "AsyncClasses", + utils::options::serializeStringList(AsyncClasses)); + Options.store(Opts, "AsyncFunctions", + utils::options::serializeStringList(AsyncFunctions)); + Options.store(Opts, "StorageClasses", + utils::options::serializeStringList(StorageClasses)); + Options.store(Opts, "StorageFunctions", + utils::options::serializeStringList(StorageFunctions)); +} + +void LambdaCaptureLifetimeCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher(cxxConstructExpr(hasDeclaration(cxxConstructorDecl( + ofClass(hasAnyName(AsyncClasses)))), + hasAnyArgument(expr())) + .bind("escape-point"), + this); + + Finder->addMatcher(callExpr(callee(functionDecl(hasAnyName(AsyncFunctions))), + hasAnyArgument(expr())) + .bind("escape-point"), + this); + + auto LongLivedStorage = anyOf(varDecl(hasGlobalStorage()), fieldDecl()); + + auto IsDirectRef = declRefExpr(to(LongLivedStorage)); + auto IsMemberRef = memberExpr(member(LongLivedStorage)); + auto StorageClass = cxxRecordDecl( + anyOf(hasAnyName(StorageClasses), + classTemplateSpecializationDecl(hasAnyName(StorageClasses)))); + + Finder->addMatcher( + cxxMemberCallExpr( + callee(cxxMethodDecl(hasAnyName(StorageFunctions), + ofClass(StorageClass))), + on(expr(ignoringParenImpCasts(anyOf(IsDirectRef, IsMemberRef)))), + hasAnyArgument(expr())) + .bind("escape-point"), + this); +} + +void LambdaCaptureLifetimeCheck::check(const MatchFinder::MatchResult &Result) { + const LambdaExpr *Lambda = nullptr; + if (const auto *Construct = + Result.Nodes.getNodeAs<CXXConstructExpr>("escape-point")) + Lambda = getEscapingLambda(Construct); + else if (const auto *Call = Result.Nodes.getNodeAs<CallExpr>("escape-point")) + Lambda = getEscapingLambda(Call); + + if (Lambda && capturesLocalVariableByReference(Lambda)) { + diag(Lambda->getBeginLoc(), + "lambda captures local variables by reference, but escapes the local " + "scope, potentially causing a use-after-free"); + } +} + +} // namespace clang::tidy::bugprone diff --git a/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.h b/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.h new file mode 100644 index 0000000000000..99a455172c2f1 --- /dev/null +++ b/clang-tools-extra/clang-tidy/bugprone/LambdaCaptureLifetimeCheck.h @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// 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_LAMBDACAPTURELIFETIMECHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LAMBDACAPTURELIFETIMECHECK_H + +#include "../ClangTidyCheck.h" +#include <vector> + +namespace clang::tidy::bugprone { + +/// Finds lambdas that capture local variables by reference and escape their +/// local scope by being passed to asynchronous sinks or out-of-scope +/// containers. +/// +/// For the user-facing documentation see: +/// https://clang.llvm.org/extra/clang-tidy/checks/bugprone/lambda-capture-lifetime.html +class LambdaCaptureLifetimeCheck : public ClangTidyCheck { +public: + LambdaCaptureLifetimeCheck(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; + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.CPlusPlus11; + } + +private: + const std::vector<StringRef> AsyncClasses; + const std::vector<StringRef> AsyncFunctions; + const std::vector<StringRef> StorageClasses; + const std::vector<StringRef> StorageFunctions; +}; + +} // namespace clang::tidy::bugprone + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_LAMBDACAPTURELIFETIMECHECK_H diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index c05d336627356..af1b1013111a2 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -219,6 +219,12 @@ New checks Finds assignments within selection statements. +- New :doc:`bugprone-lambda-capture-lifetime + <clang-tidy/checks/bugprone/lambda-capture-lifetime>` check. + + Finds lambdas that capture local variables by reference and escape their + local scope by being passed to asynchronous sinks or out-of-scope containers. + - New :doc:`bugprone-missing-end-comparison <clang-tidy/checks/bugprone/missing-end-comparison>` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/lambda-capture-lifetime.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/lambda-capture-lifetime.rst new file mode 100644 index 0000000000000..ce0753f9e0c6f --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/lambda-capture-lifetime.rst @@ -0,0 +1,59 @@ +.. title:: clang-tidy - bugprone-lambda-capture-lifetime + +bugprone-lambda-capture-lifetime +================================ + +Finds lambdas that capture local variables by reference and escape their +local scope by being passed to asynchronous sinks or out-of-scope containers, +potentially causing use-after-free bugs. + +Examples: + +.. code-block:: c++ + + #include <thread> + #include <vector> + #include <functional> + + void thread_escape() { + int local_var = 42; + // WARNING: 'local_var' is captured by reference but escapes to a thread + std::thread t([&local_var]() { + local_var++; + }); + t.detach(); + } // 'local_var' is destroyed here, causing a use-after-free in the thread. + + std::vector<std::function<void()>> GlobalActions; + + void container_escape() { + int local_var = 42; + // WARNING: 'local_var' is captured by reference but escapes to a global container + GlobalActions.push_back([&local_var]() { + local_var++; + }); + } // 'local_var' is destroyed here, but the lambda lives on in GlobalActions. + +Options +------- + +.. option:: AsyncClasses + + Semicolon-separated list of names of asynchronous classes whose constructors + are considered escape sinks. Default is ``::std::thread;::std::jthread``. + +.. option:: AsyncFunctions + + Semicolon-separated list of names of asynchronous functions that are + considered escape sinks. Default is ``::std::async``. + +.. option:: StorageClasses + + Semicolon-separated list of names of classes that act as long-lived + storage containers. Default is ``::std::vector``. + +.. option:: StorageFunctions + + Semicolon-separated list of names of member functions that store a + callable into a long-lived container. Default is + ``push_back;emplace_back;insert;assign``. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 0eb9e8a243081..aacec4aaa3116 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -114,6 +114,7 @@ Clang-Tidy Checks :doc:`bugprone-infinite-loop <bugprone/infinite-loop>`, :doc:`bugprone-integer-division <bugprone/integer-division>`, :doc:`bugprone-invalid-enum-default-initialization <bugprone/invalid-enum-default-initialization>`, + :doc:`bugprone-lambda-capture-lifetime <bugprone/lambda-capture-lifetime>`, "Yes" :doc:`bugprone-lambda-function-name <bugprone/lambda-function-name>`, :doc:`bugprone-macro-parentheses <bugprone/macro-parentheses>`, "Yes" :doc:`bugprone-macro-repeated-side-effects <bugprone/macro-repeated-side-effects>`, diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/lambda-capture-lifetime.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/lambda-capture-lifetime.cpp new file mode 100644 index 0000000000000..aa53a7fc02b48 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/lambda-capture-lifetime.cpp @@ -0,0 +1,66 @@ +// RUN: %check_clang_tidy %s bugprone-lambda-capture-lifetime %t + +namespace std { +class thread { +public: + template <typename Callable> thread(Callable&& f) {} +}; + +template <typename T> class function { +public: + function() = default; + template <typename Callable> function(Callable&& f) {} +}; + +template <typename T> +class vector { +public: + void emplace_back(T t) {} + void push_back(T t) {} +}; + +template <typename Callable> +void async(Callable&& f) {} +} // namespace std + +std::vector<std::function<int()>> GlobalFns; +std::function<int()> make_function(int); + +void test_thread() { + int x = 0; + + std::thread t1([&x]() { x = 1; }); + // CHECK-MESSAGES: :[[@LINE-1]]:18: warning: lambda captures local variables by reference, but escapes the local scope + + std::thread t2([x]() { int y = x; }); +} + +void test_async_function() { + int x = 0; + + std::async([&x]() { x = 1; }); + // CHECK-MESSAGES: :[[@LINE-1]]:14: warning: lambda captures local variables by reference, but escapes the local scope +} + +void test_vector_escape() { + int y = 0; + + GlobalFns.emplace_back([&y]() -> int { return y; }); + // CHECK-MESSAGES: :[[@LINE-1]]:26: warning: lambda captures local variables by reference, but escapes the local scope + + GlobalFns.push_back(std::function<int()>([&y]() -> int { return y; })); + // CHECK-MESSAGES: :[[@LINE-1]]:44: warning: lambda captures local variables by reference, but escapes the local scope +} + +void test_safe_local_vector() { + int z = 0; + std::vector<std::function<int()>> LocalFns; + + LocalFns.emplace_back([&z]() -> int { return z; }); +} + +void test_nested_lambda_inside_argument_does_not_escape() { + int q = 0; + + GlobalFns.emplace_back(make_function(([&q]() -> int { return q; })())); +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
