Skip to content

Conversation

@kashika0112
Copy link
Contributor

@kashika0112 kashika0112 commented Nov 27, 2025

Add lifetime annotation suggestion in lifetime analysis.

This PR introduces a new feature to Clang's lifetime analysis to detect and suggest missing [[clang::lifetimebound]] annotations on function parameters.
It introduces the concept of placeholder loans. At the entry of a function, a special placeholder loan is created for each pointer or reference parameter. The analysis then tracks these loans using OriginFlow facts. If an OriginEscapesFact shows that an origin holding a placeholder loan escapes the function's scope (e.g., via a return statement), a new warning is issued.

This warning, controlled by the warning flag -Wexperimental-lifetime-safety-suggestions, suggests adding the [[clang::lifetimebound]] attribute to the corresponding parameter.

Example:

std::string_view foo(std::string_view a) {
  return a;
}

Facts:

Function: foo
  Block B2:
    Issue (0 (Placeholder loan) , ToOrigin: 0 (Decl: a))
  End of Block
  Block B1:
    Use (0 (Decl: a), Read)
    OriginFlow (Dest: 1 (Expr: ImplicitCastExpr), Src: 0 (Decl: a))
    OriginFlow (Dest: 2 (Expr: CXXConstructExpr), Src: 1 (Expr: ImplicitCastExpr))
    OriginEscapes (2 (Expr: CXXConstructExpr))
  End of Block
  Block B0:
  End of Block

Sample warning:

o.cpp:61:39: warning: param should be marked [[clang::lifetimebound]] [-Wexperimental-lifetime-safety-suggestions]
   61 | std::string_view foo(std::string_view a) {
      |                      ~~~~~~~~~~~~~~~~~^
      |                      [[clang::lifetimebound]] 
o.cpp:62:9: note: param escapes here
   62 |  return a;

Fixes: #169939

@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 Nov 27, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 27, 2025

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Kashika Akhouri (kashika0112)

Changes

Add lifetime annotation suggestion in lifetime analysis.

This PR introduces a new feature to Clang's lifetime analysis to detect and suggest missing [[clang::lifetimebound]] annotations on function parameters.
It introduces the concept of placeholder loans. At the entry of a function, a special placeholder loan is created for each pointer or reference parameter. The analysis then tracks these loans using OriginFlow facts. If an OriginEscapesFact shows that an origin holding a placeholder loan escapes the function's scope (e.g., via a return statement), a new warning is issued.

This warning, controlled by the -Wexperimental-lifetime-suggestions flag, suggests adding the [[clang::lifetimebound]] attribute to the corresponding parameter.

Example:

std::string_view foo(std::string_view a) {
 return a;
}

Facts:

Function: foo
  Block B2:
    Issue (0 (Placeholder loan) , ToOrigin: 0 (Decl: a))
  End of Block
  Block B1:
    Use (0 (Decl: a), Read)
    OriginFlow (Dest: 1 (Expr: ImplicitCastExpr), Src: 0 (Decl: a))
    OriginFlow (Dest: 2 (Expr: CXXConstructExpr), Src: 1 (Expr: ImplicitCastExpr))
    OriginEscapes (2 (Expr: CXXConstructExpr))
  End of Block
  Block B0:
  End of Block

Sample warning:

o.cpp:61:39: warning: param should be marked [[clang::lifetimebound]] [-Wexperimental-lifetime-safety-suggestions]
   61 | std::string_view foo(std::string_view a) {
      |                      ~~~~~~~~~~~~~~~~~^
      |                      [[clang::lifetimebound]] 
o.cpp:62:9: note: param escapes here
   62 |  return a;

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

10 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+2)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h (+3)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h (+14)
  • (modified) clang/include/clang/Basic/DiagnosticGroups.td (+2)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+7)
  • (modified) clang/lib/Analysis/LifetimeSafety/Checker.cpp (+29)
  • (modified) clang/lib/Analysis/LifetimeSafety/Facts.cpp (+5-1)
  • (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+28)
  • (modified) clang/lib/Sema/AnalysisBasedWarnings.cpp (+11)
  • (modified) clang/test/Sema/warn-lifetime-safety.cpp (+71-1)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 878cb90b685f9..818133eab261d 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -91,6 +91,8 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
 
   void markUseAsWrite(const DeclRefExpr *DRE);
 
+  llvm::SmallVector<Fact *> createPlaceholderLoanFacts();
+
   FactManager &FactMgr;
   AnalysisDeclContext &AC;
   llvm::SmallVector<Fact *> CurrentBlockFacts;
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index b34a7f18b5809..5397263315010 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -47,6 +47,9 @@ class LifetimeSafetyReporter {
                                     const Expr *EscapeExpr,
                                     SourceLocation ExpiryLoc,
                                     Confidence Confidence) {}
+
+  virtual void reportMissingAnnotations(const ParmVarDecl *PVD,
+                                        const Expr *EscapeExpr) {}
 };
 
 /// The main entry point for the analysis.
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
index 7f5cf03fd3e5f..16d4c834c8071 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
@@ -67,6 +67,15 @@ class LoanManager {
   }
   llvm::ArrayRef<Loan> getLoans() const { return AllLoans; }
 
+  void addPlaceholderLoan(LoanID LID, const ParmVarDecl *PVD) {
+    PlaceholderLoans[LID] = PVD;
+  }
+
+  const llvm::DenseMap<LoanID, const ParmVarDecl *> &
+  getPlaceholderLoans() const {
+    return PlaceholderLoans;
+  }
+
 private:
   LoanID getNextLoanID() { return NextLoanID++; }
 
@@ -74,6 +83,11 @@ class LoanManager {
   /// TODO(opt): Profile and evaluate the usefullness of small buffer
   /// optimisation.
   llvm::SmallVector<Loan> AllLoans;
+  /// Represents a map of placeholder LoanID to the function parameter.
+  /// Placeholder loans are dummy loans created for each pointer or reference
+  /// parameter to represent a borrow from the function's caller, which the
+  /// analysis tracks to see if it unsafely escapes the function's scope.
+  llvm::DenseMap<LoanID, const ParmVarDecl *> PlaceholderLoans;
 };
 } // namespace clang::lifetimes::internal
 
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index 2fff32bbc4d6c..d2537870bfd64 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -541,6 +541,8 @@ def LifetimeSafety : DiagGroup<"experimental-lifetime-safety",
     Experimental warnings to detect use-after-free and related temporal safety bugs based on lifetime safety analysis.
   }];
 }
+def LifetimeSafetySuggestions
+    : DiagGroup<"experimental-lifetime-safety-suggestions">;
 
 def DistributedObjectModifiers : DiagGroup<"distributed-object-modifiers">;
 def DllexportExplicitInstantiationDecl : DiagGroup<"dllexport-explicit-instantiation-decl">;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 4a145fd71eedd..3a4949ac9a5d6 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10778,6 +10778,13 @@ def note_lifetime_safety_used_here : Note<"later used here">;
 def note_lifetime_safety_destroyed_here : Note<"destroyed here">;
 def note_lifetime_safety_returned_here : Note<"returned here">;
 
+def warn_lifetime_param_should_be_lifetimebound
+    : Warning<"param should be marked [[clang::lifetimebound]]">,
+      InGroup<LifetimeSafetySuggestions>,
+      DefaultIgnore;
+
+def note_lifetime_escapes_here : Note<"param escapes here">;
+
 // For non-floating point, expressions of the form x == x or x != x
 // should result in a warning, since these always evaluate to a constant.
 // Array comparisons have similar warnings
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 1f7c282dadac2..6b1d0f619bb4d 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -50,6 +50,7 @@ struct PendingWarning {
 class LifetimeChecker {
 private:
   llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap;
+  llvm::DenseMap<const ParmVarDecl *, const Expr *> AnnotationWarningsMap;
   const LoanPropagationAnalysis &LoanPropagation;
   const LiveOriginsAnalysis &LiveOrigins;
   const FactManager &FactMgr;
@@ -65,7 +66,28 @@ class LifetimeChecker {
       for (const Fact *F : FactMgr.getFacts(B))
         if (const auto *EF = F->getAs<ExpireFact>())
           checkExpiry(EF);
+        else if (const auto *OEF = F->getAs<OriginEscapesFact>())
+          checkAnnotations(OEF);
     issuePendingWarnings();
+    issueAnnotationWarnings();
+  }
+
+  /// Checks if an escaping origin holds a placeholder loan, indicating a
+  /// missing [[clang::lifetimebound]] annotation.
+  void checkAnnotations(const OriginEscapesFact *OEF) {
+    if (!Reporter)
+      return;
+    const auto &PlaceholderLoansMap =
+        FactMgr.getLoanMgr().getPlaceholderLoans();
+    if (PlaceholderLoansMap.empty())
+      return;
+    OriginID EscapedOID = OEF->getEscapedOriginID();
+    LoanSet EscapedLoans = LoanPropagation.getLoans(EscapedOID, OEF);
+    for (LoanID LID : EscapedLoans) {
+      if (auto It = PlaceholderLoansMap.find(LID);
+          It != PlaceholderLoansMap.end())
+        AnnotationWarningsMap.try_emplace(It->second, OEF->getEscapeExpr());
+    }
   }
 
   /// Checks for use-after-free & use-after-return errors when a loan expires.
@@ -132,6 +154,13 @@ class LifetimeChecker {
         llvm_unreachable("Unhandled CausingFact type");
     }
   }
+
+  void issueAnnotationWarnings() {
+    if (!Reporter)
+      return;
+    for (const auto &[PVD, EscapeExpr] : AnnotationWarningsMap)
+      Reporter->reportMissingAnnotations(PVD, EscapeExpr);
+  }
 };
 } // namespace
 
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index 0ae7111c489e8..823c4d19e13b9 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -19,8 +19,12 @@ void Fact::dump(llvm::raw_ostream &OS, const LoanManager &,
 
 void IssueFact::dump(llvm::raw_ostream &OS, const LoanManager &LM,
                      const OriginManager &OM) const {
+  const Loan &L = LM.getLoan(getLoanID());
   OS << "Issue (";
-  LM.getLoan(getLoanID()).dump(OS);
+  if (L.IssueExpr == nullptr)
+    OS << getLoanID() << " (Placeholder loan) ";
+  else
+    L.dump(OS);
   OS << ", ToOrigin: ";
   OM.dump(getOriginID(), OS);
   OS << ")\n";
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 00870c3fd4086..9ff4bdf4b90ff 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -42,11 +42,16 @@ static const Loan *createLoan(FactManager &FactMgr, const DeclRefExpr *DRE) {
 
 void FactsGenerator::run() {
   llvm::TimeTraceScope TimeProfile("FactGenerator");
+  const CFG &Cfg = *AC.getCFG();
+  llvm::SmallVector<Fact *> PlaceholderLoanFacts = createPlaceholderLoanFacts();
   // Iterate through the CFG blocks in reverse post-order to ensure that
   // initializations and destructions are processed in the correct sequence.
   for (const CFGBlock *Block : *AC.getAnalysis<PostOrderCFGView>()) {
     CurrentBlockFacts.clear();
     EscapesInCurrentBlock.clear();
+    if (Block->getBlockID() == Cfg.getEntry().getBlockID())
+      CurrentBlockFacts.append(PlaceholderLoanFacts.begin(),
+                               PlaceholderLoanFacts.end());
     for (unsigned I = 0; I < Block->size(); ++I) {
       const CFGElement &Element = Block->Elements[I];
       if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>())
@@ -342,4 +347,27 @@ void FactsGenerator::markUseAsWrite(const DeclRefExpr *DRE) {
   UseFacts[DRE]->markAsWritten();
 }
 
+// Creates an IssueFact for a new placeholder loan for each pointer or reference
+// parameter at the function's entry.
+llvm::SmallVector<Fact *> FactsGenerator::createPlaceholderLoanFacts() {
+  llvm::SmallVector<Fact *> PlaceholderLoanFacts;
+  const auto *FD = dyn_cast<FunctionDecl>(AC.getDecl());
+  if (!FD)
+    return PlaceholderLoanFacts;
+
+  for (const ParmVarDecl *PVD : FD->parameters()) {
+    QualType ParamType = PVD->getType();
+    if (PVD->hasAttr<LifetimeBoundAttr>())
+      continue;
+    if (ParamType->isPointerType() || ParamType->isReferenceType() ||
+        isGslPointerType(ParamType)) {
+      Loan &L = FactMgr.getLoanMgr().addLoan({PVD}, /*IssueExpr=*/nullptr);
+      FactMgr.getLoanMgr().addPlaceholderLoan(L.ID, PVD);
+      OriginID OID = FactMgr.getOriginMgr().getOrCreate(*PVD);
+      PlaceholderLoanFacts.push_back(FactMgr.createFact<IssueFact>(L.ID, OID));
+    }
+  }
+  return PlaceholderLoanFacts;
+}
+
 } // namespace clang::lifetimes::internal
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 43d2b9a829545..666f8dabbd4cb 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -2884,6 +2884,17 @@ class LifetimeSafetyReporterImpl : public LifetimeSafetyReporter {
         << EscapeExpr->getEndLoc();
   }
 
+  void reportMissingAnnotations(const ParmVarDecl *PVD,
+                                const Expr *EscapeExpr) override {
+    S.Diag(PVD->getLocation(),
+           diag::warn_lifetime_param_should_be_lifetimebound)
+        << PVD->getSourceRange()
+        << FixItHint::CreateInsertion(
+               PVD->getTypeSourceInfo()->getTypeLoc().getBeginLoc(),
+               "[[clang::lifetimebound]] ");
+    S.Diag(EscapeExpr->getBeginLoc(), diag::note_lifetime_escapes_here);
+  }
+
 private:
   Sema &S;
 };
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 1191469e23df1..4318ec4c4cb2f 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety -Wexperimental-lifetime-safety -Wno-dangling -verify %s
+// RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety -Wexperimental-lifetime-safety -Wexperimental-lifetime-safety-suggestions -Wno-dangling -verify %s
 
 struct MyObj {
   int id;
@@ -943,3 +943,73 @@ void parentheses(bool cond) {
   }  // expected-note 4 {{destroyed here}}
   (void)*p;  // expected-note 4 {{later used here}}
 }
+
+//===----------------------------------------------------------------------===//
+// Lifetimebound Annotation Suggestion Tests
+//===----------------------------------------------------------------------===//
+
+View return_view_directly (View a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a; // expected-note {{param escapes here}}
+}
+
+View conditional_return_view (
+    View a, // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+    View b,  // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+    bool c
+) {
+  View res;
+  if (c)  
+    res = a;                    
+  else
+    res = b;          
+  return res;  // expected-note 2 {{param escapes here}} 
+}
+
+// FIXME: Fails to generate lifetime suggestion for reference types as these are not handled currently.
+MyObj& return_reference (
+  MyObj& a,   
+  MyObj& b,   
+  bool c
+) {
+  if(c) {
+    return a;   
+  }
+  return b;     
+}
+
+// FIXME: Fails to generate lifetime suggestion for reference types as these are not handled currently.
+View return_view_from_reference (
+    MyObj& p 
+) {
+  return p; 
+}
+
+int* return_pointer_directly (int* a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a;               // expected-note {{param escapes here}} 
+}
+
+MyObj* return_pointer_object (MyObj* a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a;                           // expected-note {{param escapes here}} 
+}
+
+View only_one_paramter_annotated (View a [[clang::lifetimebound]], 
+  View b, // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+  bool c
+) {
+ if(c)
+  return a;
+ return b; // expected-note {{param escapes here}} 
+}
+
+// Safe cases
+View already_annotated(View a [[clang::lifetimebound]]) {
+ return a;
+}
+
+// Safe cases
+MyObj return_obj_by_value(MyObj& p) {
+  return p;
+}

@llvmbot
Copy link
Member

llvmbot commented Nov 27, 2025

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

Author: Kashika Akhouri (kashika0112)

Changes

Add lifetime annotation suggestion in lifetime analysis.

This PR introduces a new feature to Clang's lifetime analysis to detect and suggest missing [[clang::lifetimebound]] annotations on function parameters.
It introduces the concept of placeholder loans. At the entry of a function, a special placeholder loan is created for each pointer or reference parameter. The analysis then tracks these loans using OriginFlow facts. If an OriginEscapesFact shows that an origin holding a placeholder loan escapes the function's scope (e.g., via a return statement), a new warning is issued.

This warning, controlled by the -Wexperimental-lifetime-suggestions flag, suggests adding the [[clang::lifetimebound]] attribute to the corresponding parameter.

Example:

std::string_view foo(std::string_view a) {
 return a;
}

Facts:

Function: foo
  Block B2:
    Issue (0 (Placeholder loan) , ToOrigin: 0 (Decl: a))
  End of Block
  Block B1:
    Use (0 (Decl: a), Read)
    OriginFlow (Dest: 1 (Expr: ImplicitCastExpr), Src: 0 (Decl: a))
    OriginFlow (Dest: 2 (Expr: CXXConstructExpr), Src: 1 (Expr: ImplicitCastExpr))
    OriginEscapes (2 (Expr: CXXConstructExpr))
  End of Block
  Block B0:
  End of Block

Sample warning:

o.cpp:61:39: warning: param should be marked [[clang::lifetimebound]] [-Wexperimental-lifetime-safety-suggestions]
   61 | std::string_view foo(std::string_view a) {
      |                      ~~~~~~~~~~~~~~~~~^
      |                      [[clang::lifetimebound]] 
o.cpp:62:9: note: param escapes here
   62 |  return a;

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

10 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+2)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h (+3)
  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h (+14)
  • (modified) clang/include/clang/Basic/DiagnosticGroups.td (+2)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+7)
  • (modified) clang/lib/Analysis/LifetimeSafety/Checker.cpp (+29)
  • (modified) clang/lib/Analysis/LifetimeSafety/Facts.cpp (+5-1)
  • (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+28)
  • (modified) clang/lib/Sema/AnalysisBasedWarnings.cpp (+11)
  • (modified) clang/test/Sema/warn-lifetime-safety.cpp (+71-1)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 878cb90b685f9..818133eab261d 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -91,6 +91,8 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
 
   void markUseAsWrite(const DeclRefExpr *DRE);
 
+  llvm::SmallVector<Fact *> createPlaceholderLoanFacts();
+
   FactManager &FactMgr;
   AnalysisDeclContext &AC;
   llvm::SmallVector<Fact *> CurrentBlockFacts;
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
index b34a7f18b5809..5397263315010 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h
@@ -47,6 +47,9 @@ class LifetimeSafetyReporter {
                                     const Expr *EscapeExpr,
                                     SourceLocation ExpiryLoc,
                                     Confidence Confidence) {}
+
+  virtual void reportMissingAnnotations(const ParmVarDecl *PVD,
+                                        const Expr *EscapeExpr) {}
 };
 
 /// The main entry point for the analysis.
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
index 7f5cf03fd3e5f..16d4c834c8071 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
@@ -67,6 +67,15 @@ class LoanManager {
   }
   llvm::ArrayRef<Loan> getLoans() const { return AllLoans; }
 
+  void addPlaceholderLoan(LoanID LID, const ParmVarDecl *PVD) {
+    PlaceholderLoans[LID] = PVD;
+  }
+
+  const llvm::DenseMap<LoanID, const ParmVarDecl *> &
+  getPlaceholderLoans() const {
+    return PlaceholderLoans;
+  }
+
 private:
   LoanID getNextLoanID() { return NextLoanID++; }
 
@@ -74,6 +83,11 @@ class LoanManager {
   /// TODO(opt): Profile and evaluate the usefullness of small buffer
   /// optimisation.
   llvm::SmallVector<Loan> AllLoans;
+  /// Represents a map of placeholder LoanID to the function parameter.
+  /// Placeholder loans are dummy loans created for each pointer or reference
+  /// parameter to represent a borrow from the function's caller, which the
+  /// analysis tracks to see if it unsafely escapes the function's scope.
+  llvm::DenseMap<LoanID, const ParmVarDecl *> PlaceholderLoans;
 };
 } // namespace clang::lifetimes::internal
 
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index 2fff32bbc4d6c..d2537870bfd64 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -541,6 +541,8 @@ def LifetimeSafety : DiagGroup<"experimental-lifetime-safety",
     Experimental warnings to detect use-after-free and related temporal safety bugs based on lifetime safety analysis.
   }];
 }
+def LifetimeSafetySuggestions
+    : DiagGroup<"experimental-lifetime-safety-suggestions">;
 
 def DistributedObjectModifiers : DiagGroup<"distributed-object-modifiers">;
 def DllexportExplicitInstantiationDecl : DiagGroup<"dllexport-explicit-instantiation-decl">;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 4a145fd71eedd..3a4949ac9a5d6 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10778,6 +10778,13 @@ def note_lifetime_safety_used_here : Note<"later used here">;
 def note_lifetime_safety_destroyed_here : Note<"destroyed here">;
 def note_lifetime_safety_returned_here : Note<"returned here">;
 
+def warn_lifetime_param_should_be_lifetimebound
+    : Warning<"param should be marked [[clang::lifetimebound]]">,
+      InGroup<LifetimeSafetySuggestions>,
+      DefaultIgnore;
+
+def note_lifetime_escapes_here : Note<"param escapes here">;
+
 // For non-floating point, expressions of the form x == x or x != x
 // should result in a warning, since these always evaluate to a constant.
 // Array comparisons have similar warnings
diff --git a/clang/lib/Analysis/LifetimeSafety/Checker.cpp b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
index 1f7c282dadac2..6b1d0f619bb4d 100644
--- a/clang/lib/Analysis/LifetimeSafety/Checker.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Checker.cpp
@@ -50,6 +50,7 @@ struct PendingWarning {
 class LifetimeChecker {
 private:
   llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap;
+  llvm::DenseMap<const ParmVarDecl *, const Expr *> AnnotationWarningsMap;
   const LoanPropagationAnalysis &LoanPropagation;
   const LiveOriginsAnalysis &LiveOrigins;
   const FactManager &FactMgr;
@@ -65,7 +66,28 @@ class LifetimeChecker {
       for (const Fact *F : FactMgr.getFacts(B))
         if (const auto *EF = F->getAs<ExpireFact>())
           checkExpiry(EF);
+        else if (const auto *OEF = F->getAs<OriginEscapesFact>())
+          checkAnnotations(OEF);
     issuePendingWarnings();
+    issueAnnotationWarnings();
+  }
+
+  /// Checks if an escaping origin holds a placeholder loan, indicating a
+  /// missing [[clang::lifetimebound]] annotation.
+  void checkAnnotations(const OriginEscapesFact *OEF) {
+    if (!Reporter)
+      return;
+    const auto &PlaceholderLoansMap =
+        FactMgr.getLoanMgr().getPlaceholderLoans();
+    if (PlaceholderLoansMap.empty())
+      return;
+    OriginID EscapedOID = OEF->getEscapedOriginID();
+    LoanSet EscapedLoans = LoanPropagation.getLoans(EscapedOID, OEF);
+    for (LoanID LID : EscapedLoans) {
+      if (auto It = PlaceholderLoansMap.find(LID);
+          It != PlaceholderLoansMap.end())
+        AnnotationWarningsMap.try_emplace(It->second, OEF->getEscapeExpr());
+    }
   }
 
   /// Checks for use-after-free & use-after-return errors when a loan expires.
@@ -132,6 +154,13 @@ class LifetimeChecker {
         llvm_unreachable("Unhandled CausingFact type");
     }
   }
+
+  void issueAnnotationWarnings() {
+    if (!Reporter)
+      return;
+    for (const auto &[PVD, EscapeExpr] : AnnotationWarningsMap)
+      Reporter->reportMissingAnnotations(PVD, EscapeExpr);
+  }
 };
 } // namespace
 
diff --git a/clang/lib/Analysis/LifetimeSafety/Facts.cpp b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
index 0ae7111c489e8..823c4d19e13b9 100644
--- a/clang/lib/Analysis/LifetimeSafety/Facts.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Facts.cpp
@@ -19,8 +19,12 @@ void Fact::dump(llvm::raw_ostream &OS, const LoanManager &,
 
 void IssueFact::dump(llvm::raw_ostream &OS, const LoanManager &LM,
                      const OriginManager &OM) const {
+  const Loan &L = LM.getLoan(getLoanID());
   OS << "Issue (";
-  LM.getLoan(getLoanID()).dump(OS);
+  if (L.IssueExpr == nullptr)
+    OS << getLoanID() << " (Placeholder loan) ";
+  else
+    L.dump(OS);
   OS << ", ToOrigin: ";
   OM.dump(getOriginID(), OS);
   OS << ")\n";
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 00870c3fd4086..9ff4bdf4b90ff 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -42,11 +42,16 @@ static const Loan *createLoan(FactManager &FactMgr, const DeclRefExpr *DRE) {
 
 void FactsGenerator::run() {
   llvm::TimeTraceScope TimeProfile("FactGenerator");
+  const CFG &Cfg = *AC.getCFG();
+  llvm::SmallVector<Fact *> PlaceholderLoanFacts = createPlaceholderLoanFacts();
   // Iterate through the CFG blocks in reverse post-order to ensure that
   // initializations and destructions are processed in the correct sequence.
   for (const CFGBlock *Block : *AC.getAnalysis<PostOrderCFGView>()) {
     CurrentBlockFacts.clear();
     EscapesInCurrentBlock.clear();
+    if (Block->getBlockID() == Cfg.getEntry().getBlockID())
+      CurrentBlockFacts.append(PlaceholderLoanFacts.begin(),
+                               PlaceholderLoanFacts.end());
     for (unsigned I = 0; I < Block->size(); ++I) {
       const CFGElement &Element = Block->Elements[I];
       if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>())
@@ -342,4 +347,27 @@ void FactsGenerator::markUseAsWrite(const DeclRefExpr *DRE) {
   UseFacts[DRE]->markAsWritten();
 }
 
+// Creates an IssueFact for a new placeholder loan for each pointer or reference
+// parameter at the function's entry.
+llvm::SmallVector<Fact *> FactsGenerator::createPlaceholderLoanFacts() {
+  llvm::SmallVector<Fact *> PlaceholderLoanFacts;
+  const auto *FD = dyn_cast<FunctionDecl>(AC.getDecl());
+  if (!FD)
+    return PlaceholderLoanFacts;
+
+  for (const ParmVarDecl *PVD : FD->parameters()) {
+    QualType ParamType = PVD->getType();
+    if (PVD->hasAttr<LifetimeBoundAttr>())
+      continue;
+    if (ParamType->isPointerType() || ParamType->isReferenceType() ||
+        isGslPointerType(ParamType)) {
+      Loan &L = FactMgr.getLoanMgr().addLoan({PVD}, /*IssueExpr=*/nullptr);
+      FactMgr.getLoanMgr().addPlaceholderLoan(L.ID, PVD);
+      OriginID OID = FactMgr.getOriginMgr().getOrCreate(*PVD);
+      PlaceholderLoanFacts.push_back(FactMgr.createFact<IssueFact>(L.ID, OID));
+    }
+  }
+  return PlaceholderLoanFacts;
+}
+
 } // namespace clang::lifetimes::internal
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 43d2b9a829545..666f8dabbd4cb 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -2884,6 +2884,17 @@ class LifetimeSafetyReporterImpl : public LifetimeSafetyReporter {
         << EscapeExpr->getEndLoc();
   }
 
+  void reportMissingAnnotations(const ParmVarDecl *PVD,
+                                const Expr *EscapeExpr) override {
+    S.Diag(PVD->getLocation(),
+           diag::warn_lifetime_param_should_be_lifetimebound)
+        << PVD->getSourceRange()
+        << FixItHint::CreateInsertion(
+               PVD->getTypeSourceInfo()->getTypeLoc().getBeginLoc(),
+               "[[clang::lifetimebound]] ");
+    S.Diag(EscapeExpr->getBeginLoc(), diag::note_lifetime_escapes_here);
+  }
+
 private:
   Sema &S;
 };
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index 1191469e23df1..4318ec4c4cb2f 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety -Wexperimental-lifetime-safety -Wno-dangling -verify %s
+// RUN: %clang_cc1 -fsyntax-only -fexperimental-lifetime-safety -Wexperimental-lifetime-safety -Wexperimental-lifetime-safety-suggestions -Wno-dangling -verify %s
 
 struct MyObj {
   int id;
@@ -943,3 +943,73 @@ void parentheses(bool cond) {
   }  // expected-note 4 {{destroyed here}}
   (void)*p;  // expected-note 4 {{later used here}}
 }
+
+//===----------------------------------------------------------------------===//
+// Lifetimebound Annotation Suggestion Tests
+//===----------------------------------------------------------------------===//
+
+View return_view_directly (View a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a; // expected-note {{param escapes here}}
+}
+
+View conditional_return_view (
+    View a, // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+    View b,  // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+    bool c
+) {
+  View res;
+  if (c)  
+    res = a;                    
+  else
+    res = b;          
+  return res;  // expected-note 2 {{param escapes here}} 
+}
+
+// FIXME: Fails to generate lifetime suggestion for reference types as these are not handled currently.
+MyObj& return_reference (
+  MyObj& a,   
+  MyObj& b,   
+  bool c
+) {
+  if(c) {
+    return a;   
+  }
+  return b;     
+}
+
+// FIXME: Fails to generate lifetime suggestion for reference types as these are not handled currently.
+View return_view_from_reference (
+    MyObj& p 
+) {
+  return p; 
+}
+
+int* return_pointer_directly (int* a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a;               // expected-note {{param escapes here}} 
+}
+
+MyObj* return_pointer_object (MyObj* a // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+) {
+  return a;                           // expected-note {{param escapes here}} 
+}
+
+View only_one_paramter_annotated (View a [[clang::lifetimebound]], 
+  View b, // expected-warning {{param should be marked [[clang::lifetimebound]]}}.
+  bool c
+) {
+ if(c)
+  return a;
+ return b; // expected-note {{param escapes here}} 
+}
+
+// Safe cases
+View already_annotated(View a [[clang::lifetimebound]]) {
+ return a;
+}
+
+// Safe cases
+MyObj return_obj_by_value(MyObj& p) {
+  return p;
+}

@usx95 usx95 self-requested a review November 27, 2025 07:20
@github-actions
Copy link

github-actions bot commented Nov 27, 2025

🐧 Linux x64 Test Results

  • 111606 tests passed
  • 4467 tests skipped

Copy link
Collaborator

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks amazing! Thanks for looking into this! Most of my comments are about making sure this change is a strong basis for some future improvements that we might want to make.


for (const ParmVarDecl *PVD : FD->parameters()) {
QualType ParamType = PVD->getType();
if (PVD->hasAttr<LifetimeBoundAttr>())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not necessarily need to address this now, but I think we might want to have loans for the lifetimebound annotated parameters as well. This would help as find invalidation style issues in the future:

int* func(std::vector<int>& v [[clang::lifetimebound]]) {
  v.push_back(0);
  return v[0];
}

Also, having loans for annotated parameters would help us potentially find erroneous annotations:

int* func(std::vector<int>& v1 [[clang::lifetimebound]], // oops, added to the wrong parameter.
          std::vector<int>& v2) {
  return v2[0];
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. The reporting at the end should take care of not suggesting already annotated params instead of dropping the placeholder loans.

These loans would also help us improve C++ coverage metric: "Origins with no loans". It seems helpful to differentiate between the case when origins hold placeholders vs origins hold no loans (because of being either unknown of analysis bug). So these loans serve mutliple purposes beyond lifetime suggestions alone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, have moved this check to the reporting function.

@usx95 usx95 changed the title Suggest lifetime annotations [Issue Id: 169375] [LifetimeSafety] Suggest lifetime annotations Nov 27, 2025
Copy link
Contributor

@usx95 usx95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks amazing. I have dropped comments mostly for some structural changes.


for (const ParmVarDecl *PVD : FD->parameters()) {
QualType ParamType = PVD->getType();
if (PVD->hasAttr<LifetimeBoundAttr>())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. The reporting at the end should take care of not suggesting already annotated params instead of dropping the placeholder loans.

These loans would also help us improve C++ coverage metric: "Origins with no loans". It seems helpful to differentiate between the case when origins hold placeholders vs origins hold no loans (because of being either unknown of analysis bug). So these loans serve mutliple purposes beyond lifetime suggestions alone.

Copy link
Collaborator

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks!

There is one significant feature that would be nice to support, detecting missing lifetimebound annotations for the implicit object parameter! I think that is probably better be addressed in a follow-up though and that might be a bit more involved.

@kashika0112 kashika0112 requested a review from usx95 November 28, 2025 15:30
Copy link
Contributor

@usx95 usx95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. LGTM.

Last few comments:

  • I would suggest to build LLVM using this to be sure we do not introduce crashes. Maybe see if we have some suggestions already.
  • Filed a bug for implicit this #169941. Please add these tests with a FIXME quoting the bug.

}
}

void issueAnnotationWarnings() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to suggestAnnotations

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.

Suggest [[clang::lifetimebound]] for function parameters of view type

4 participants