[clang] [LifetimeSafety] Add implicit tracking for STL functions (PR #170005)
Utkarsh Saxena via cfe-commits
cfe-commits at lists.llvm.org
Thu Jan 8 02:13:30 PST 2026
https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/170005
>From 8ed4841b649868c2196c39fa2538c481c24d9c86 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Sat, 29 Nov 2025 15:07:40 +0000
Subject: [PATCH] Implicit lifetimebound for std namespace
---
.../LifetimeSafety/LifetimeAnnotations.h | 21 ++
.../LifetimeSafety/FactsGenerator.cpp | 5 +-
.../LifetimeSafety/LifetimeAnnotations.cpp | 82 ++++++++
clang/lib/Analysis/LifetimeSafety/Origins.cpp | 4 +
clang/lib/Sema/CheckExprLifetime.cpp | 94 +--------
.../unittests/Analysis/LifetimeSafetyTest.cpp | 180 ++++++++++++++++++
6 files changed, 297 insertions(+), 89 deletions(-)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index 1a16fb82f9a84..f96d412aa63d2 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -14,6 +14,13 @@
namespace clang ::lifetimes {
+// This function is needed because Decl::isInStdNamespace will return false for
+// iterators in some STL implementations due to them being defined in a
+// namespace outside of the std namespace.
+bool isInStlNamespace(const Decl *D);
+
+bool isPointerLikeType(QualType QT);
+
/// Returns the most recent declaration of the method to ensure all
/// lifetime-bound attributes from redeclarations are considered.
const FunctionDecl *getDeclWithMergedLifetimeBoundAttrs(const FunctionDecl *FD);
@@ -38,6 +45,20 @@ bool isAssignmentOperatorLifetimeBound(const CXXMethodDecl *CMD);
/// method or because it's a normal assignment operator.
bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD);
+// Returns true if the implicit object argument (this) of a method call should
+// be tracked for GSL lifetime analysis. This applies to STL methods that return
+// pointers or references that depend on the lifetime of the object, such as
+// container iterators (begin, end), data accessors (c_str, data, get), or
+// element accessors (operator[], operator*, front, back, at).
+bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee);
+
+// Returns true if the first argument of a free function should be tracked for
+// GSL lifetime analysis. This applies to STL free functions that take a pointer
+// to a GSL Owner or Pointer and return a pointer or reference that depends on
+// the lifetime of the argument, such as std::begin, std::data, std::get, or
+// std::any_cast.
+bool shouldTrackFirstArgument(const FunctionDecl *FD);
+
// Tells whether the type is annotated with [[gsl::Pointer]].
bool isGslPointerType(QualType QT);
// Tells whether the type is annotated with [[gsl::Owner]].
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 546fe37491040..f3993b7e7e261 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -410,11 +410,14 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
Method && Method->isInstance()) {
if (I == 0)
// For the 'this' argument, the attribute is on the method itself.
- return implicitObjectParamIsLifetimeBound(Method);
+ return implicitObjectParamIsLifetimeBound(Method) ||
+ shouldTrackImplicitObjectArg(Method);
if ((I - 1) < Method->getNumParams())
// For explicit arguments, find the corresponding parameter
// declaration.
PVD = Method->getParamDecl(I - 1);
+ } else if (I == 0 && shouldTrackFirstArgument(FD)) {
+ return true;
} else if (I < FD->getNumParams()) {
// For free functions or static methods.
PVD = FD->getParamDecl(I);
diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
index 54e343fc2ee5e..2772fe20de19b 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
@@ -71,6 +71,88 @@ bool implicitObjectParamIsLifetimeBound(const FunctionDecl *FD) {
return isNormalAssignmentOperator(FD);
}
+bool isInStlNamespace(const Decl *D) {
+ const DeclContext *DC = D->getDeclContext();
+ if (!DC)
+ return false;
+ if (const auto *ND = dyn_cast<NamespaceDecl>(DC))
+ if (const IdentifierInfo *II = ND->getIdentifier()) {
+ StringRef Name = II->getName();
+ if (Name.size() >= 2 && Name.front() == '_' &&
+ (Name[1] == '_' || isUppercase(Name[1])))
+ return true;
+ }
+ return DC->isStdNamespace();
+}
+
+bool isPointerLikeType(QualType QT) {
+ return isGslPointerType(QT) || QT->isPointerType() || QT->isNullPtrType();
+}
+
+bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
+ if (auto *Conv = dyn_cast_or_null<CXXConversionDecl>(Callee))
+ if (isGslPointerType(Conv->getConversionType()) &&
+ Callee->getParent()->hasAttr<OwnerAttr>())
+ return true;
+ if (!isInStlNamespace(Callee->getParent()))
+ return false;
+ if (!isGslPointerType(Callee->getFunctionObjectParameterType()) &&
+ !isGslOwnerType(Callee->getFunctionObjectParameterType()))
+ return false;
+ if (isPointerLikeType(Callee->getReturnType())) {
+ if (!Callee->getIdentifier())
+ return false;
+ return llvm::StringSwitch<bool>(Callee->getName())
+ .Cases(
+ {// Begin and end iterators.
+ "begin", "end", "rbegin", "rend", "cbegin", "cend", "crbegin",
+ "crend",
+ // Inner pointer getters.
+ "c_str", "data", "get",
+ // Map and set types.
+ "find", "equal_range", "lower_bound", "upper_bound"},
+ true)
+ .Default(false);
+ }
+ if (Callee->getReturnType()->isReferenceType()) {
+ if (!Callee->getIdentifier()) {
+ auto OO = Callee->getOverloadedOperator();
+ if (!Callee->getParent()->hasAttr<OwnerAttr>())
+ return false;
+ return OO == OverloadedOperatorKind::OO_Subscript ||
+ OO == OverloadedOperatorKind::OO_Star;
+ }
+ return llvm::StringSwitch<bool>(Callee->getName())
+ .Cases({"front", "back", "at", "top", "value"}, true)
+ .Default(false);
+ }
+ return false;
+}
+
+bool shouldTrackFirstArgument(const FunctionDecl *FD) {
+ if (!FD->getIdentifier() || FD->getNumParams() != 1)
+ return false;
+ const auto *RD = FD->getParamDecl(0)->getType()->getPointeeCXXRecordDecl();
+ if (!FD->isInStdNamespace() || !RD || !RD->isInStdNamespace())
+ return false;
+ if (!RD->hasAttr<PointerAttr>() && !RD->hasAttr<OwnerAttr>())
+ return false;
+ if (FD->getReturnType()->isPointerType() ||
+ isGslPointerType(FD->getReturnType())) {
+ return llvm::StringSwitch<bool>(FD->getName())
+ .Cases({"begin", "rbegin", "cbegin", "crbegin"}, true)
+ .Cases({"end", "rend", "cend", "crend"}, true)
+ .Case("data", true)
+ .Default(false);
+ }
+ if (FD->getReturnType()->isReferenceType()) {
+ return llvm::StringSwitch<bool>(FD->getName())
+ .Cases({"get", "any_cast"}, true)
+ .Default(false);
+ }
+ return false;
+}
+
template <typename T> static bool isRecordWithAttr(QualType Type) {
auto *RD = Type->getAsCXXRecordDecl();
if (!RD)
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index 2c1deac03d24b..f7153f23cbfd5 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -12,6 +12,7 @@
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/AST/TypeBase.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
@@ -124,6 +125,9 @@ OriginList *OriginManager::getOrCreateList(const ValueDecl *D) {
OriginList *OriginManager::getOrCreateList(const Expr *E) {
if (auto *ParenIgnored = E->IgnoreParens(); ParenIgnored != E)
return getOrCreateList(ParenIgnored);
+ // We do not see CFG stmts for ExprWithCleanups. Simply peel them.
+ if (auto *EC = dyn_cast<ExprWithCleanups>(E))
+ return getOrCreateList(EC->getSubExpr());
if (!hasOrigins(E))
return nullptr;
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c91ca751984c9..9396b4d9d5ba7 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -260,28 +260,6 @@ static void visitLocalsRetainedByReferenceBinding(IndirectLocalPath &Path,
Expr *Init, ReferenceKind RK,
LocalVisitor Visit);
-static bool isPointerLikeType(QualType QT) {
- return isGslPointerType(QT) || QT->isPointerType() || QT->isNullPtrType();
-}
-
-// Decl::isInStdNamespace will return false for iterators in some STL
-// implementations due to them being defined in a namespace outside of the std
-// namespace.
-static bool isInStlNamespace(const Decl *D) {
- const DeclContext *DC = D->getDeclContext();
- if (!DC)
- return false;
- if (const auto *ND = dyn_cast<NamespaceDecl>(DC))
- if (const IdentifierInfo *II = ND->getIdentifier()) {
- StringRef Name = II->getName();
- if (Name.size() >= 2 && Name.front() == '_' &&
- (Name[1] == '_' || isUppercase(Name[1])))
- return true;
- }
-
- return DC->isStdNamespace();
-}
-
// Returns true if the given Record decl is a form of `GSLOwner<Pointer>`
// type, e.g. std::vector<string_view>, std::optional<string_view>.
static bool isContainerOfPointer(const RecordDecl *Container) {
@@ -291,7 +269,7 @@ static bool isContainerOfPointer(const RecordDecl *Container) {
return false;
const auto &TAs = CTSD->getTemplateArgs();
return TAs.size() > 0 && TAs[0].getKind() == TemplateArgument::Type &&
- isPointerLikeType(TAs[0].getAsType());
+ lifetimes::isPointerLikeType(TAs[0].getAsType());
}
return false;
}
@@ -312,70 +290,10 @@ static bool isStdInitializerListOfPointer(const RecordDecl *RD) {
if (const auto *CTSD =
dyn_cast_if_present<ClassTemplateSpecializationDecl>(RD)) {
const auto &TAs = CTSD->getTemplateArgs();
- return isInStlNamespace(RD) && RD->getIdentifier() &&
+ return lifetimes::isInStlNamespace(RD) && RD->getIdentifier() &&
RD->getName() == "initializer_list" && TAs.size() > 0 &&
TAs[0].getKind() == TemplateArgument::Type &&
- isPointerLikeType(TAs[0].getAsType());
- }
- return false;
-}
-
-static bool shouldTrackImplicitObjectArg(const CXXMethodDecl *Callee) {
- if (auto *Conv = dyn_cast_or_null<CXXConversionDecl>(Callee))
- if (isGslPointerType(Conv->getConversionType()) &&
- Callee->getParent()->hasAttr<OwnerAttr>())
- return true;
- if (!isInStlNamespace(Callee->getParent()))
- return false;
- if (!isGslPointerType(Callee->getFunctionObjectParameterType()) &&
- !isGslOwnerType(Callee->getFunctionObjectParameterType()))
- return false;
- if (isPointerLikeType(Callee->getReturnType())) {
- if (!Callee->getIdentifier())
- return false;
- return llvm::StringSwitch<bool>(Callee->getName())
- .Cases({"begin", "rbegin", "cbegin", "crbegin"}, true)
- .Cases({"end", "rend", "cend", "crend"}, true)
- .Cases({"c_str", "data", "get"}, true)
- // Map and set types.
- .Cases({"find", "equal_range", "lower_bound", "upper_bound"}, true)
- .Default(false);
- }
- if (Callee->getReturnType()->isReferenceType()) {
- if (!Callee->getIdentifier()) {
- auto OO = Callee->getOverloadedOperator();
- if (!Callee->getParent()->hasAttr<OwnerAttr>())
- return false;
- return OO == OverloadedOperatorKind::OO_Subscript ||
- OO == OverloadedOperatorKind::OO_Star;
- }
- return llvm::StringSwitch<bool>(Callee->getName())
- .Cases({"front", "back", "at", "top", "value"}, true)
- .Default(false);
- }
- return false;
-}
-
-static bool shouldTrackFirstArgument(const FunctionDecl *FD) {
- if (!FD->getIdentifier() || FD->getNumParams() != 1)
- return false;
- const auto *RD = FD->getParamDecl(0)->getType()->getPointeeCXXRecordDecl();
- if (!FD->isInStdNamespace() || !RD || !RD->isInStdNamespace())
- return false;
- if (!RD->hasAttr<PointerAttr>() && !RD->hasAttr<OwnerAttr>())
- return false;
- if (FD->getReturnType()->isPointerType() ||
- isGslPointerType(FD->getReturnType())) {
- return llvm::StringSwitch<bool>(FD->getName())
- .Cases({"begin", "rbegin", "cbegin", "crbegin"}, true)
- .Cases({"end", "rend", "cend", "crend"}, true)
- .Case("data", true)
- .Default(false);
- }
- if (FD->getReturnType()->isReferenceType()) {
- return llvm::StringSwitch<bool>(FD->getName())
- .Cases({"get", "any_cast"}, true)
- .Default(false);
+ lifetimes::isPointerLikeType(TAs[0].getAsType());
}
return false;
}
@@ -564,7 +482,7 @@ static void visitFunctionCallArguments(IndirectLocalPath &Path, Expr *Call,
VisitLifetimeBoundArg(Callee, ObjectArg);
else if (EnableGSLAnalysis) {
if (auto *CME = dyn_cast<CXXMethodDecl>(Callee);
- CME && shouldTrackImplicitObjectArg(CME))
+ CME && lifetimes::shouldTrackImplicitObjectArg(CME))
VisitGSLPointerArg(Callee, ObjectArg);
}
}
@@ -605,7 +523,7 @@ static void visitFunctionCallArguments(IndirectLocalPath &Path, Expr *Call,
VisitLifetimeBoundArg(CanonCallee->getParamDecl(I), Arg);
else if (EnableGSLAnalysis && I == 0) {
// Perform GSL analysis for the first argument
- if (shouldTrackFirstArgument(CanonCallee)) {
+ if (lifetimes::shouldTrackFirstArgument(CanonCallee)) {
VisitGSLPointerArg(CanonCallee, Arg);
} else if (auto *Ctor = dyn_cast<CXXConstructExpr>(Call);
Ctor && shouldTrackFirstArgumentForConstructor(Ctor)) {
@@ -1532,7 +1450,7 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
break;
}
case LK_LifetimeCapture: {
- if (isPointerLikeType(Init->getType()))
+ if (lifetimes::isPointerLikeType(Init->getType()))
Path.push_back({IndirectLocalPathEntry::GslPointerInit, Init});
break;
}
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index f05a249c373c4..b1f74a7a2c850 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -1598,5 +1598,185 @@ TEST_F(LifetimeAnalysisTest, TrivialClassDestructorsUAR) {
EXPECT_THAT("s", HasLiveLoanAtExpiry("p1"));
}
+// ========================================================================= //
+// Tests for shouldTrackImplicitObjectArg
+// ========================================================================= //
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_STLBegin) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct vector {
+ struct iterator {};
+ iterator begin();
+ };
+ }
+
+ void target() {
+ std::vector<int> vec;
+ auto it = vec.begin();
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("it"), HasLoansTo({"vec"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_OwnerDeref) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct optional {
+ T& operator*();
+ };
+ }
+
+ void target() {
+ std::optional<int> opt;
+ int& r = *opt;
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("r"), HasLoansTo({"opt"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_Value) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct optional {
+ T& value();
+ };
+ }
+
+ void target() {
+ std::optional<int> opt;
+ int& r = opt.value();
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("r"), HasLoansTo({"opt"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_UniquePtr_Get) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct unique_ptr {
+ T *get() const;
+ };
+ }
+
+ void target() {
+ std::unique_ptr<int> up;
+ int* r = up.get();
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("r"), HasLoansTo({"up"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_ConversionOperator) {
+ SetupTest(R"(
+ struct [[gsl::Pointer(int)]] IntPtr {
+ int& operator*();
+ };
+
+ struct [[gsl::Owner(int)]] OwnerWithConversion {
+ operator IntPtr();
+ };
+
+ void target() {
+ OwnerWithConversion owner;
+ IntPtr ptr = owner;
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("ptr"), HasLoansTo({"owner"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackImplicitObjectArg_MapFind) {
+ SetupTest(R"(
+ namespace std {
+ template<typename K, typename V>
+ struct map {
+ struct iterator {};
+ iterator find(const K&);
+ };
+ }
+
+ void target() {
+ std::map<int, int> m;
+ auto it = m.find(42);
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("it"), HasLoansTo({"m"}, "p1"));
+}
+
+// ========================================================================= //
+// Tests for shouldTrackFirstArgument
+// ========================================================================= //
+
+TEST_F(LifetimeAnalysisTest, TrackFirstArgument_StdBegin) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct vector {
+ struct iterator {};
+ iterator begin();
+ };
+
+ template<typename C>
+ auto begin(C& c) -> decltype(c.begin());
+ }
+
+ void target() {
+ std::vector<int> vec;
+ auto it = std::begin(vec);
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("it"), HasLoansTo({"vec"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackFirstArgument_StdData) {
+ SetupTest(R"(
+ namespace std {
+ template<typename T>
+ struct vector {
+ const T* data() const;
+ };
+
+ template<typename C>
+ auto data(C& c) -> decltype(c.data());
+ }
+
+ void target() {
+ std::vector<int> vec;
+ const int* p = std::data(vec);
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"vec"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, TrackFirstArgument_StdAnyCast) {
+ SetupTest(R"(
+ namespace std {
+ struct any {};
+
+ template<typename T>
+ T any_cast(const any& op);
+ }
+
+ void target() {
+ std::any a;
+ int& r = std::any_cast<int&>(a);
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("r"), HasLoansTo({"a"}, "p1"));
+}
+
} // anonymous namespace
} // namespace clang::lifetimes::internal
More information about the cfe-commits
mailing list