llvmbot wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

<details>
<summary>Changes</summary>

This PR extends the lifetime safety analysis to handle GSL pointer types 
(classes marked with `[[gsl::Pointer()]]` attribute), enabling detection of 
dangling references in pointer-like types such as `string_view` or other view 
types.

Key improvements:

- Added detection of borrows created through GSL pointer constructors
- Implemented tracking of origin propagation through GSL pointer types
- Added support for various initialization patterns (direct, list, functional 
cast)
- Implemented handling of materialized temporaries for GSL pointer types
- Added comprehensive test cases for GSL pointer behavior

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


3 Files Affected:

- (modified) clang/lib/Analysis/LifetimeSafety.cpp (+120-8) 
- (modified) clang/test/Sema/warn-lifetime-safety.cpp (+110-10) 
- (modified) clang/unittests/Analysis/LifetimeSafetyTest.cpp (+156-1) 


``````````diff
diff --git a/clang/lib/Analysis/LifetimeSafety.cpp 
b/clang/lib/Analysis/LifetimeSafety.cpp
index 00efa4c5e548f..f9afa3ff6b5d9 100644
--- a/clang/lib/Analysis/LifetimeSafety.cpp
+++ b/clang/lib/Analysis/LifetimeSafety.cpp
@@ -433,6 +433,31 @@ class FactGeneratorVisitor : public 
ConstStmtVisitor<FactGeneratorVisitor> {
 
   void VisitDeclRefExpr(const DeclRefExpr *DRE) { handleUse(DRE); }
 
+  void VisitCXXConstructExpr(const CXXConstructExpr *CCE) {
+    if (!isGslPointerType(CCE->getType()))
+      return;
+
+    if (CCE->getNumArgs() > 0 && hasOrigin(CCE->getArg(0)->getType()))
+      // This is a propagation.
+      addAssignOriginFact(*CCE, *CCE->getArg(0));
+    else
+      // This could be a new borrow.
+      checkForBorrows(CCE, CCE->getConstructor(),
+                      {CCE->getArgs(), CCE->getNumArgs()});
+  }
+
+  void VisitCXXMemberCallExpr(const CXXMemberCallExpr *MCE) {
+    if (!isGslPointerType(MCE->getImplicitObjectArgument()->getType()))
+      return;
+    // Specifically for conversion operators, like `std::string_view p = a;`
+    if (isa<CXXConversionDecl>(MCE->getCalleeDecl())) {
+      // The argument is the implicit object itself.
+      checkForBorrows(MCE, MCE->getMethodDecl(),
+                      {MCE->getImplicitObjectArgument()});
+    }
+    // Note: A more general VisitCallExpr could also be used here.
+  }
+
   void VisitCXXNullPtrLiteralExpr(const CXXNullPtrLiteralExpr *N) {
     /// TODO: Handle nullptr expr as a special 'null' loan. Uninitialized
     /// pointers can use the same type of loan.
@@ -492,10 +517,27 @@ class FactGeneratorVisitor : public 
ConstStmtVisitor<FactGeneratorVisitor> {
   void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE) {
     // Check if this is a test point marker. If so, we are done with this
     // expression.
-    if (VisitTestPoint(FCE))
+    if (handleTestPoint(FCE))
       return;
-    // Visit as normal otherwise.
-    Base::VisitCXXFunctionalCastExpr(FCE);
+    if (isGslPointerType(FCE->getType()))
+      addAssignOriginFact(*FCE, *FCE->getSubExpr());
+  }
+
+  void VisitInitListExpr(const InitListExpr *ILE) {
+    if (!hasOrigin(ILE->getType()))
+      return;
+    // For list initialization with a single element, like `View{...}`, the
+    // origin of the list itself is the origin of its single element.
+    if (ILE->getNumInits() == 1)
+      addAssignOriginFact(*ILE, *ILE->getInit(0));
+  }
+
+  void VisitMaterializeTemporaryExpr(const MaterializeTemporaryExpr *MTE) {
+    if (!hasOrigin(MTE->getType()))
+      return;
+    // A temporary object's origin is the same as the origin of the
+    // expression that initializes it.
+    addAssignOriginFact(*MTE, *MTE->getSubExpr());
   }
 
   void handleDestructor(const CFGAutomaticObjDtor &DtorOpt) {
@@ -521,8 +563,75 @@ class FactGeneratorVisitor : public 
ConstStmtVisitor<FactGeneratorVisitor> {
   }
 
 private:
+  static bool isGslPointerType(QualType QT) {
+    if (const auto *RD = QT->getAsCXXRecordDecl()) {
+      // We need to check the template definition for specializations.
+      if (auto *CTSD = dyn_cast<ClassTemplateSpecializationDecl>(RD))
+        return CTSD->getSpecializedTemplate()
+            ->getTemplatedDecl()
+            ->hasAttr<PointerAttr>();
+      return RD->hasAttr<PointerAttr>();
+    }
+    return false;
+  }
+
   // Check if a type has an origin.
-  bool hasOrigin(QualType QT) { return QT->isPointerOrReferenceType(); }
+  static bool hasOrigin(QualType QT) {
+    if (QT->isFunctionPointerType())
+      return false;
+    return QT->isPointerOrReferenceType() || isGslPointerType(QT);
+  }
+
+  /// Checks if a call-like expression creates a borrow by passing a local
+  /// value to a reference parameter, creating an IssueFact if it does.
+  void checkForBorrows(const Expr *Call, const FunctionDecl *FD,
+                       ArrayRef<const Expr *> Args) {
+    if (!FD)
+      return;
+
+    for (unsigned I = 0; I < Args.size(); ++I) {
+      if (I >= FD->getNumParams())
+        break;
+
+      const ParmVarDecl *Param = FD->getParamDecl(I);
+      const Expr *Arg = Args[I];
+
+      // This is the core condition for a new borrow: a value type (no origin)
+      // is passed to a reference parameter.
+      if (Param->getType()->isReferenceType() && !hasOrigin(Arg->getType())) {
+        if (const Loan *L = createLoanFrom(Arg, Call)) {
+          OriginID OID = FactMgr.getOriginMgr().getOrCreate(*Call);
+          CurrentBlockFacts.push_back(
+              FactMgr.createFact<IssueFact>(L->ID, OID));
+          // For view creation, we assume the first borrow is the significant
+          // one.
+          return;
+        }
+      }
+    }
+  }
+
+  /// Attempts to create a loan by analyzing the source expression of a borrow.
+  /// This method is the single point for creating loans, allowing for future
+  /// expansion to handle temporaries, field members, etc.
+  /// \param SourceExpr The expression representing the object being borrowed
+  /// from.
+  /// \param IssueExpr The expression that triggers the borrow (e.g., a
+  /// constructor call).
+  /// \return The new Loan on success, nullptr on failure.
+  const Loan *createLoanFrom(const Expr *SourceExpr, const Expr *IssueExpr) {
+    // For now, we only handle direct borrows from local variables.
+    // In the future, this can be extended to handle MaterializeTemporaryExpr,
+    // etc.
+    if (const auto *DRE =
+            dyn_cast<DeclRefExpr>(SourceExpr->IgnoreParenImpCasts())) {
+      if (const auto *VD = dyn_cast<ValueDecl>(DRE->getDecl())) {
+        AccessPath Path(VD);
+        return &FactMgr.getLoanMgr().addLoan(Path, IssueExpr);
+      }
+    }
+    return nullptr;
+  }
 
   template <typename Destination, typename Source>
   void addAssignOriginFact(const Destination &D, const Source &S) {
@@ -534,7 +643,7 @@ class FactGeneratorVisitor : public 
ConstStmtVisitor<FactGeneratorVisitor> {
 
   /// Checks if the expression is a `void("__lifetime_test_point_...")` cast.
   /// If so, creates a `TestPointFact` and returns true.
-  bool VisitTestPoint(const CXXFunctionalCastExpr *FCE) {
+  bool handleTestPoint(const CXXFunctionalCastExpr *FCE) {
     if (!FCE->getType()->isVoidType())
       return false;
 
@@ -612,10 +721,13 @@ class FactGenerator : public 
RecursiveASTVisitor<FactGenerator> {
     for (const CFGBlock *Block : *AC.getAnalysis<PostOrderCFGView>()) {
       FactGeneratorBlockRAII BlockGenerator(FG, Block);
       for (const CFGElement &Element : *Block) {
-        if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>())
+        if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>()) {
+          DEBUG_WITH_TYPE("PrintCFG", llvm::dbgs() << " ================== 
\n");
+          DEBUG_WITH_TYPE("PrintCFG", CS->dump());
+          DEBUG_WITH_TYPE("PrintCFG", CS->getStmt()->dumpColor());
           TraverseStmt(const_cast<Stmt *>(CS->getStmt()));
-        else if (std::optional<CFGAutomaticObjDtor> DtorOpt =
-                     Element.getAs<CFGAutomaticObjDtor>())
+        } else if (std::optional<CFGAutomaticObjDtor> DtorOpt =
+                       Element.getAs<CFGAutomaticObjDtor>())
           FG.handleDestructor(*DtorOpt);
       }
     }
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp 
b/clang/test/Sema/warn-lifetime-safety.cpp
index 660b9c9d5e243..bc8a5f3f7150f 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -6,6 +6,12 @@ struct MyObj {
   MyObj operator+(MyObj);
 };
 
+struct [[gsl::Pointer()]] View {
+  View(const MyObj&); // Borrows from MyObj
+  View();
+  void use() const;
+};
+
 
//===----------------------------------------------------------------------===//
 // Basic Definite Use-After-Free (-W...permissive)
 // These are cases where the pointer is guaranteed to be dangling at the use 
site.
@@ -20,12 +26,31 @@ void definite_simple_case() {
   (void)*p;     // expected-note {{later used here}}
 }
 
+void definite_simple_case_gsl() {
+  View v;
+  {
+    MyObj s;
+    v = s;      // expected-warning {{object whose reference is captured does 
not live long enough}}
+  }             // expected-note {{destroyed here}}
+  v.use();      // expected-note {{later used here}}
+}
+
 void no_use_no_error() {
   MyObj* p;
   {
     MyObj s;
     p = &s;
   }
+  // 'p' is dangling here, but since it is never used, no warning is issued.
+}
+
+void no_use_no_error_gsl() {
+  View v;
+  {
+    MyObj s;
+    v = s;
+  }
+  // 'v' is dangling here, but since it is never used, no warning is issued.
 }
 
 void definite_pointer_chain() {
@@ -39,6 +64,16 @@ void definite_pointer_chain() {
   (void)*q;     // expected-note {{later used here}}
 }
 
+void definite_propagation_gsl() {
+  View v1, v2;
+  {
+    MyObj s;
+    v1 = s;     // expected-warning {{object whose reference is captured does 
not live long enough}}
+    v2 = v1;
+  }             // expected-note {{destroyed here}}
+  v2.use();     // expected-note {{later used here}}
+}
+
 void definite_multiple_uses_one_warning() {
   MyObj* p;
   {
@@ -78,6 +113,19 @@ void definite_single_pointer_multiple_loans(bool cond) {
   (void)*p;     // expected-note 2  {{later used here}}
 }
 
+void definite_single_pointer_multiple_loans_gsl(bool cond) {
+  View v;
+  if (cond){
+    MyObj s;
+    v = s;      // expected-warning {{object whose reference is captured does 
not live long enough}}
+  }             // expected-note {{destroyed here}}
+  else {
+    MyObj t;
+    v = t;      // expected-warning {{object whose reference is captured does 
not live long enough}}
+  }             // expected-note {{destroyed here}}
+  v.use();      // expected-note 2 {{later used here}}
+}
+
 
 
//===----------------------------------------------------------------------===//
 // Potential (Maybe) Use-After-Free (-W...strict)
@@ -94,18 +142,14 @@ void potential_if_branch(bool cond) {
   (void)*p;     // expected-note {{later used here}}
 }
 
-// If all paths lead to a dangle, it becomes a definite error.
-void potential_becomes_definite(bool cond) {
-  MyObj* p;
+void potential_if_branch_gsl(bool cond) {
+  MyObj safe;
+  View v = safe;
   if (cond) {
-    MyObj temp1;
-    p = &temp1; // expected-warning {{does not live long enough}}
-  }             // expected-note {{destroyed here}}
-  else {      
-    MyObj temp2;
-    p = &temp2; // expected-warning {{does not live long enough}}
+    MyObj temp;
+    v = temp;   // expected-warning {{object whose reference is captured may 
not live long enough}}
   }             // expected-note {{destroyed here}}
-  (void)*p;     // expected-note 2 {{later used here}}
+  v.use();      // expected-note {{later used here}}
 }
 
 void definite_potential_together(bool cond) {
@@ -159,6 +203,16 @@ void potential_for_loop_use_after_loop_body(MyObj safe) {
   (void)*p;     // expected-note {{later used here}}
 }
 
+void potential_for_loop_gsl() {
+  MyObj safe;
+  View v = safe;
+  for (int i = 0; i < 1; ++i) {
+    MyObj s;
+    v = s;      // expected-warning {{object whose reference is captured may 
not live long enough}}
+  }             // expected-note {{destroyed here}}
+  v.use();      // expected-note {{later used here}}
+}
+
 void potential_for_loop_use_before_loop_body(MyObj safe) {
   MyObj* p = &safe;
   for (int i = 0; i < 1; ++i) {
@@ -182,6 +236,19 @@ void potential_loop_with_break(bool cond) {
   (void)*p;     // expected-note {{later used here}}
 }
 
+void potential_loop_with_break_gsl(bool cond) {
+  MyObj safe;
+  View v = safe;
+  for (int i = 0; i < 10; ++i) {
+    if (cond) {
+      MyObj temp;
+      v = temp;   // expected-warning {{object whose reference is captured may 
not live long enough}}
+      break;      // expected-note {{destroyed here}}
+    }
+  }
+  v.use();      // expected-note {{later used here}}
+}
+
 void potential_multiple_expiry_of_same_loan(bool cond) {
   // Choose the last expiry location for the loan.
   MyObj safe;
@@ -258,6 +325,28 @@ void definite_switch(int mode) {
   (void)*p;     // expected-note 3 {{later used here}}
 }
 
+void definite_switch_gsl(int mode) {
+  View v;
+  switch (mode) {
+  case 1: {
+    MyObj temp1;
+    v = temp1;  // expected-warning {{object whose reference is captured does 
not live long enough}}
+    break;      // expected-note {{destroyed here}}
+  }
+  case 2: {
+    MyObj temp2;
+    v = temp2;  // expected-warning {{object whose reference is captured does 
not live long enough}}
+    break;      // expected-note {{destroyed here}}
+  }
+  default: {
+    MyObj temp3;
+    v = temp3;  // expected-warning {{object whose reference is captured does 
not live long enough}}
+    break;      // expected-note {{destroyed here}}
+  }
+  }
+  v.use();      // expected-note 3 {{later used here}}
+}
+
 
//===----------------------------------------------------------------------===//
 // No-Error Cases
 
//===----------------------------------------------------------------------===//
@@ -271,3 +360,14 @@ void no_error_if_dangle_then_rescue() {
   p = &safe;    // p is "rescued" before use.
   (void)*p;     // This is safe.
 }
+
+void no_error_if_dangle_then_rescue_gsl() {
+  MyObj safe;
+  View v;
+  {
+    MyObj temp;
+    v = temp;  // 'v' is temporarily dangling.
+  }
+  v = safe;    // 'v' is "rescued" before use by reassigning to a valid object.
+  v.use();     // This is safe.
+}
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp 
b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
index 13e5832d70050..98862a1a157b8 100644
--- a/clang/unittests/Analysis/LifetimeSafetyTest.cpp
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -11,7 +11,6 @@
 #include "clang/ASTMatchers/ASTMatchers.h"
 #include "clang/Testing/TestAST.h"
 #include "llvm/ADT/StringMap.h"
-#include "llvm/Testing/Support/Error.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <optional>
@@ -31,7 +30,13 @@ class LifetimeTestRunner {
   LifetimeTestRunner(llvm::StringRef Code) {
     std::string FullCode = R"(
       #define POINT(name) void("__lifetime_test_point_" #name)
+
       struct MyObj { ~MyObj() {} int i; };
+
+      struct [[gsl::Pointer()]] View { 
+        View(const MyObj&);
+        View();
+      };
     )";
     FullCode += Code.str();
 
@@ -741,5 +746,155 @@ TEST_F(LifetimeAnalysisTest, 
NoDuplicateLoansForImplicitCastToConst) {
   EXPECT_THAT(Helper->getLoansForVar("a"), SizeIs(2));
 }
 
+TEST_F(LifetimeAnalysisTest, GslPointerSimpleLoan) {
+  SetupTest(R"(
+    void target() {
+      MyObj a;
+      View x = a;
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("x"), HasLoansTo({"a"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerConstructFromOwner) {
+  SetupTest(R"(
+    void target() {
+      MyObj al, bl, cl, dl, el, fl;
+      View a = View(al);
+      View b = View{bl};
+      View c = View(View(View(cl)));
+      View d = View{View(View(dl))};
+      View e = View{View{View{el}}};
+      View f = {fl};
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("a"), HasLoansTo({"al"}, "p1"));
+  EXPECT_THAT(Origin("b"), HasLoansTo({"bl"}, "p1"));
+  EXPECT_THAT(Origin("c"), HasLoansTo({"cl"}, "p1"));
+  EXPECT_THAT(Origin("d"), HasLoansTo({"dl"}, "p1"));
+  EXPECT_THAT(Origin("e"), HasLoansTo({"el"}, "p1"));
+  EXPECT_THAT(Origin("f"), HasLoansTo({"fl"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerConstructFromView) {
+  SetupTest(R"(
+    void target() {
+      MyObj a;
+      View x = View(a);
+      View y = View{x};
+      View z = View(View(View(y)));
+      View p = View{View(View(x))};
+      View q = {x};
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("x"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("y"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("z"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("p"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("q"), HasLoansTo({"a"}, "p1"));
+}
+
+// FIXME: Handle loans in ternary operator!
+TEST_F(LifetimeAnalysisTest, GslPointerInConditionalOperator) {
+  SetupTest(R"(
+    void target(bool cond) {
+      MyObj a, b;
+      View v = cond ? a : b;
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("v"), HasLoansTo({}, "p1"));
+}
+
+// FIXME: Handle temporaries.
+TEST_F(LifetimeAnalysisTest, ViewFromTemporary) {
+  SetupTest(R"(
+    MyObj temporary();
+    void target() {
+      View v = temporary();
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("v"), HasLoansTo({}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerWithConstAndAuto) {
+  SetupTest(R"(
+    void target() {
+      MyObj a;
+      const View v1 = a;
+      auto v2 = v1;
+      const auto& v3 = v2;
+      POINT(p1);
+    }
+  )");
+  EXPECT_THAT(Origin("v1"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("v2"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("v3"), HasLoansTo({"a"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerPropagation) {
+  SetupTest(R"(
+    void target() {
+      MyObj a;
+      View x = a;
+      POINT(p1);
+
+      View y = x; // Propagation via copy-construction
+      POINT(p2);
+
+      View z;
+      z = x;       // Propagation via copy-assignment
+      POINT(p3);
+    }
+  )");
+
+  EXPECT_THAT(Origin("x"), HasLoansTo({"a"}, "p1"));
+  EXPECT_THAT(Origin("y"), HasLoansTo({"a"}, "p2"));
+  EXPECT_THAT(Origin("z"), HasLoansTo({"a"}, "p3"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerLoanExpiration) {
+  SetupTest(R"(
+    void target() {
+      View x;
+      {
+        MyObj a;
+        x = a;
+        POINT(before_expiry);
+      } // `a` is destroyed here.
+      POINT(after_expiry);
+    }
+  )");
+
+  EXPECT_THAT(NoLoans(), AreExpiredAt("before_expiry"));
+  EXPECT_THAT(LoansTo({"a"}), AreExpiredAt("after_expiry"));
+}
+
+TEST_F(LifetimeAnalysisTest, GslPointerReassignment) {
+  SetupTest(R"(
+    void target() {
+      MyObj safe;
+      View v;
+      v = safe;
+      POINT(p1);
+      {
+        MyObj unsafe;
+        v = unsafe;
+        POINT(p2);
+      } // `unsafe` expires here.
+      POINT(p3);
+    }
+  )");
+
+  EXPECT_THAT(Origin("v"), HasLoansTo({"safe"}, "p1"));
+  EXPECT_THAT(Origin("v"), HasLoansTo({"unsafe"}, "p2"));
+  EXPECT_THAT(Origin("v"), HasLoansTo({"unsafe"}, "p3"));
+  EXPECT_THAT(LoansTo({"unsafe"}), AreExpiredAt("p3"));
+}
+
 } // anonymous namespace
 } // namespace clang::lifetimes::internal

``````````

</details>


https://github.com/llvm/llvm-project/pull/154009
_______________________________________________
llvm-branch-commits mailing list
llvm-branch-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits

Reply via email to