Skip to content

Conversation

@usx95
Copy link
Contributor

@usx95 usx95 commented Nov 29, 2025

Add support for tracking STL container methods and free functions in the lifetime safety analysis.

  • Added VisitExprWithCleanups to the FactsGenerator to properly handle expressions with cleanup code
  • Moved shouldTrackImplicitObjectArg and shouldTrackFirstArgument from CheckExprLifetime.cpp to LifetimeAnnotations.h/cpp to make them available to the lifetime safety analysis
  • Enhanced the lifetime analysis to track STL container methods that return pointers or references dependent on the container's lifetime (e.g., begin(), data(), c_str())
  • Added support for tracking free functions like std::begin, std::data, and std::any_cast that return pointers or references dependent on their arguments

Fixes #162622

Copy link
Contributor Author

usx95 commented Nov 29, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@usx95 usx95 force-pushed the users/usx95/11-29-implicit_lifetimebound_for_std_namespace branch from 363a78c to f19ea1b Compare December 2, 2025 17:38
@usx95 usx95 force-pushed the users/usx95/11-17-lifetime-safety-multi-origin branch from 3e3ccdc to 67d76ba Compare December 2, 2025 17:38
@usx95 usx95 changed the title Implicit lifetimebound for std namespace [LifetimeSafety] Move shouldTrack functions to LifetimeAnnotations Dec 3, 2025
@usx95 usx95 force-pushed the users/usx95/11-17-lifetime-safety-multi-origin branch from 67d76ba to 96bad25 Compare December 3, 2025 10:23
@usx95 usx95 force-pushed the users/usx95/11-29-implicit_lifetimebound_for_std_namespace branch from f19ea1b to 6b70da3 Compare December 3, 2025 10:23
@github-actions
Copy link

github-actions bot commented Dec 3, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@usx95 usx95 changed the title [LifetimeSafety] Move shouldTrack functions to LifetimeAnnotations [LifetimeSafety] Add implicit tracking for STL functions Dec 3, 2025
@usx95 usx95 force-pushed the users/usx95/11-29-implicit_lifetimebound_for_std_namespace branch 2 times, most recently from a71af86 to 651cb0b Compare December 3, 2025 10:33
@usx95 usx95 marked this pull request as ready for review December 3, 2025 10:34
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:analysis clang:temporal-safety Issue/FR relating to the lifetime analysis in Clang (-Wdangling, -Wreturn-local-addr) labels Dec 3, 2025
@usx95 usx95 requested review from Xazax-hun, hokein and ymand December 3, 2025 10:35
@llvmbot
Copy link
Member

llvmbot commented Dec 3, 2025

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

Changes

Add support for tracking STL container methods and free functions in the lifetime safety analysis.

  • Added VisitExprWithCleanups to the FactsGenerator to properly handle expressions with cleanup code
  • Moved shouldTrackImplicitObjectArg and shouldTrackFirstArgument from CheckExprLifetime.cpp to LifetimeAnnotations.h/cpp to make them available to the lifetime safety analysis
  • Enhanced the lifetime analysis to track STL container methods that return pointers or references dependent on the container's lifetime (e.g., begin(), data(), c_str())
  • Added support for tracking free functions like std::begin, std::data, and std::any_cast that return pointers or references dependent on their arguments

Full diff: https://github.com/llvm/llvm-project/pull/170005.diff

7 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+1)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h (+14)
  • (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+11-1)
  • (modified) clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp (+82)
  • (modified) clang/lib/Analysis/LifetimeSafety/Origins.cpp (+3)
  • (modified) clang/lib/Sema/CheckExprLifetime.cpp (+2-62)
  • (modified) clang/unittests/Analysis/LifetimeSafetyTest.cpp (+181)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 5b5626020e772..d3ef72a8d12dd 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -48,6 +48,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
   void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE);
   void VisitInitListExpr(const InitListExpr *ILE);
   void VisitMaterializeTemporaryExpr(const MaterializeTemporaryExpr *MTE);
+  void VisitExprWithCleanups(const ExprWithCleanups *EC);
 
 private:
   OriginList *getOriginsList(const ValueDecl &D);
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index 1a16fb82f9a84..8e26a4d41a957 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -38,6 +38,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 460be3ad99347..62b42a8dfb1b8 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -14,6 +14,7 @@
 #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 #include "clang/Analysis/Analyses/PostOrderCFGView.h"
 #include "llvm/Support/Casting.h"
+#include "llvm/Support/Debug.h"
 #include "llvm/Support/Signals.h"
 #include "llvm/Support/TimeProfiler.h"
 
@@ -328,6 +329,12 @@ void FactsGenerator::VisitMaterializeTemporaryExpr(
   }
 }
 
+void FactsGenerator::VisitExprWithCleanups(const ExprWithCleanups *EC) {
+  if (hasOrigins(EC)) {
+    killAndFlowOrigin(*EC, *EC->getSubExpr());
+  }
+}
+
 void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
   /// TODO: Handle loans to temporaries.
   const VarDecl *LifetimeEndsVD = LifetimeEnds.getVarDecl();
@@ -387,11 +394,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..860aa5373a32c 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);
 }
 
+// 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();
+}
+
+static 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", "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;
+}
+
+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 ac8d8041f600b..65fc0c21e11b8 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -11,6 +11,7 @@
 #include "clang/AST/Attr.h"
 #include "clang/AST/DeclCXX.h"
 #include "clang/AST/DeclTemplate.h"
+#include "clang/AST/ExprCXX.h"
 #include "clang/AST/TypeBase.h"
 #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 
@@ -79,6 +80,8 @@ OriginList *OriginManager::getOrCreateList(const ValueDecl *D) {
 OriginList *OriginManager::getOrCreateList(const Expr *E, size_t Depth) {
   if (auto *ParenIgnored = E->IgnoreParens(); ParenIgnored != E)
     return getOrCreateList(ParenIgnored);
+  if (auto *EC = dyn_cast<ExprWithCleanups>(E))
+    return getOrCreateList(EC->getSubExpr(), Depth);
 
   if (!hasOrigins(E))
     return nullptr;
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c91ca751984c9..26e4d75b1fa49 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -320,66 +320,6 @@ static bool isStdInitializerListOfPointer(const RecordDecl *RD) {
   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);
-  }
-  return false;
-}
-
 // Returns true if the given constructor is a copy-like constructor, such as
 // `Ctor(Owner<U>&&)` or `Ctor(const Owner<U>&)`.
 static bool isCopyLikeConstructor(const CXXConstructorDecl *Ctor) {
@@ -564,7 +504,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 +545,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)) {
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index ddc9cb602fc26..553354f052875 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -1597,5 +1597,186 @@ 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

@llvmbot
Copy link
Member

llvmbot commented Dec 3, 2025

@llvm/pr-subscribers-clang-temporal-safety

Author: Utkarsh Saxena (usx95)

Changes

Add support for tracking STL container methods and free functions in the lifetime safety analysis.

  • Added VisitExprWithCleanups to the FactsGenerator to properly handle expressions with cleanup code
  • Moved shouldTrackImplicitObjectArg and shouldTrackFirstArgument from CheckExprLifetime.cpp to LifetimeAnnotations.h/cpp to make them available to the lifetime safety analysis
  • Enhanced the lifetime analysis to track STL container methods that return pointers or references dependent on the container's lifetime (e.g., begin(), data(), c_str())
  • Added support for tracking free functions like std::begin, std::data, and std::any_cast that return pointers or references dependent on their arguments

Full diff: https://github.com/llvm/llvm-project/pull/170005.diff

7 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+1)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h (+14)
  • (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+11-1)
  • (modified) clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp (+82)
  • (modified) clang/lib/Analysis/LifetimeSafety/Origins.cpp (+3)
  • (modified) clang/lib/Sema/CheckExprLifetime.cpp (+2-62)
  • (modified) clang/unittests/Analysis/LifetimeSafetyTest.cpp (+181)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 5b5626020e772..d3ef72a8d12dd 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -48,6 +48,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
   void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE);
   void VisitInitListExpr(const InitListExpr *ILE);
   void VisitMaterializeTemporaryExpr(const MaterializeTemporaryExpr *MTE);
+  void VisitExprWithCleanups(const ExprWithCleanups *EC);
 
 private:
   OriginList *getOriginsList(const ValueDecl &D);
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index 1a16fb82f9a84..8e26a4d41a957 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -38,6 +38,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 460be3ad99347..62b42a8dfb1b8 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -14,6 +14,7 @@
 #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 #include "clang/Analysis/Analyses/PostOrderCFGView.h"
 #include "llvm/Support/Casting.h"
+#include "llvm/Support/Debug.h"
 #include "llvm/Support/Signals.h"
 #include "llvm/Support/TimeProfiler.h"
 
@@ -328,6 +329,12 @@ void FactsGenerator::VisitMaterializeTemporaryExpr(
   }
 }
 
+void FactsGenerator::VisitExprWithCleanups(const ExprWithCleanups *EC) {
+  if (hasOrigins(EC)) {
+    killAndFlowOrigin(*EC, *EC->getSubExpr());
+  }
+}
+
 void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
   /// TODO: Handle loans to temporaries.
   const VarDecl *LifetimeEndsVD = LifetimeEnds.getVarDecl();
@@ -387,11 +394,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..860aa5373a32c 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);
 }
 
+// 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();
+}
+
+static 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", "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;
+}
+
+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 ac8d8041f600b..65fc0c21e11b8 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -11,6 +11,7 @@
 #include "clang/AST/Attr.h"
 #include "clang/AST/DeclCXX.h"
 #include "clang/AST/DeclTemplate.h"
+#include "clang/AST/ExprCXX.h"
 #include "clang/AST/TypeBase.h"
 #include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
 
@@ -79,6 +80,8 @@ OriginList *OriginManager::getOrCreateList(const ValueDecl *D) {
 OriginList *OriginManager::getOrCreateList(const Expr *E, size_t Depth) {
   if (auto *ParenIgnored = E->IgnoreParens(); ParenIgnored != E)
     return getOrCreateList(ParenIgnored);
+  if (auto *EC = dyn_cast<ExprWithCleanups>(E))
+    return getOrCreateList(EC->getSubExpr(), Depth);
 
   if (!hasOrigins(E))
     return nullptr;
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c91ca751984c9..26e4d75b1fa49 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -320,66 +320,6 @@ static bool isStdInitializerListOfPointer(const RecordDecl *RD) {
   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);
-  }
-  return false;
-}
-
 // Returns true if the given constructor is a copy-like constructor, such as
 // `Ctor(Owner<U>&&)` or `Ctor(const Owner<U>&)`.
 static bool isCopyLikeConstructor(const CXXConstructorDecl *Ctor) {
@@ -564,7 +504,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 +545,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)) {
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index ddc9cb602fc26..553354f052875 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -1597,5 +1597,186 @@ 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

@usx95 usx95 force-pushed the users/usx95/11-29-implicit_lifetimebound_for_std_namespace branch from 651cb0b to ddc91c3 Compare December 3, 2025 10:36
@usx95 usx95 force-pushed the users/usx95/11-29-implicit_lifetimebound_for_std_namespace branch from ddc91c3 to 2ac7726 Compare December 3, 2025 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:analysis clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:temporal-safety Issue/FR relating to the lifetime analysis in Clang (-Wdangling, -Wreturn-local-addr) clang Clang issues not falling into any other category

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants