Skip to content

Commit 5a74f7e

Browse files
authored
[LifetimeSafety] Suggest lifetime annotations (#169767)
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: ```cpp 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
1 parent 5b2eb0d commit 5a74f7e

File tree

12 files changed

+297
-32
lines changed

12 files changed

+297
-32
lines changed

clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
9191

9292
void markUseAsWrite(const DeclRefExpr *DRE);
9393

94+
llvm::SmallVector<Fact *> issuePlaceholderLoans();
9495
FactManager &FactMgr;
9596
AnalysisDeclContext &AC;
9697
llvm::SmallVector<Fact *> CurrentBlockFacts;

clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeSafety.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class LifetimeSafetyReporter {
4747
const Expr *EscapeExpr,
4848
SourceLocation ExpiryLoc,
4949
Confidence Confidence) {}
50+
51+
// Suggests lifetime bound annotations for function paramters
52+
virtual void suggestAnnotation(const ParmVarDecl *PVD,
53+
const Expr *EscapeExpr) {}
5054
};
5155

5256
/// The main entry point for the analysis.

clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,46 +34,115 @@ struct AccessPath {
3434
AccessPath(const clang::ValueDecl *D) : D(D) {}
3535
};
3636

37-
/// Information about a single borrow, or "Loan". A loan is created when a
38-
/// reference or pointer is created.
39-
struct Loan {
37+
/// An abstract base class for a single "Loan" which represents lending a
38+
/// storage in memory.
39+
class Loan {
4040
/// TODO: Represent opaque loans.
4141
/// TODO: Represent nullptr: loans to no path. Accessing it UB! Currently it
4242
/// is represented as empty LoanSet
43-
LoanID ID;
43+
public:
44+
enum class Kind : uint8_t {
45+
/// A loan with an access path to a storage location.
46+
Path,
47+
/// A non-expiring placeholder loan for a parameter, representing a borrow
48+
/// from the function's caller.
49+
Placeholder
50+
};
51+
52+
Loan(Kind K, LoanID ID) : K(K), ID(ID) {}
53+
virtual ~Loan() = default;
54+
55+
Kind getKind() const { return K; }
56+
LoanID getID() const { return ID; }
57+
58+
virtual void dump(llvm::raw_ostream &OS) const = 0;
59+
60+
private:
61+
const Kind K;
62+
const LoanID ID;
63+
};
64+
65+
/// PathLoan represents lending a storage location that is visible within the
66+
/// function's scope (e.g., a local variable on stack).
67+
class PathLoan : public Loan {
4468
AccessPath Path;
4569
/// The expression that creates the loan, e.g., &x.
4670
const Expr *IssueExpr;
4771

48-
Loan(LoanID id, AccessPath path, const Expr *IssueExpr)
49-
: ID(id), Path(path), IssueExpr(IssueExpr) {}
72+
public:
73+
PathLoan(LoanID ID, AccessPath Path, const Expr *IssueExpr)
74+
: Loan(Kind::Path, ID), Path(Path), IssueExpr(IssueExpr) {}
75+
76+
const AccessPath &getAccessPath() const { return Path; }
77+
const Expr *getIssueExpr() const { return IssueExpr; }
5078

51-
void dump(llvm::raw_ostream &OS) const;
79+
void dump(llvm::raw_ostream &OS) const override;
80+
81+
static bool classof(const Loan *L) { return L->getKind() == Kind::Path; }
82+
};
83+
84+
/// A placeholder loan held by a function parameter, representing a borrow from
85+
/// the caller's scope.
86+
///
87+
/// Created at function entry for each pointer or reference parameter with an
88+
/// origin. Unlike PathLoan, placeholder loans:
89+
/// - Have no IssueExpr (created at function entry, not at a borrow site)
90+
/// - Have no AccessPath (the borrowed object is not visible to the function)
91+
/// - Do not currently expire, but may in the future when modeling function
92+
/// invalidations (e.g., vector::push_back)
93+
///
94+
/// When a placeholder loan escapes the function (e.g., via return), it
95+
/// indicates the parameter should be marked [[clang::lifetimebound]], enabling
96+
/// lifetime annotation suggestions.
97+
class PlaceholderLoan : public Loan {
98+
/// The function parameter that holds this placeholder loan.
99+
const ParmVarDecl *PVD;
100+
101+
public:
102+
PlaceholderLoan(LoanID ID, const ParmVarDecl *PVD)
103+
: Loan(Kind::Placeholder, ID), PVD(PVD) {}
104+
105+
const ParmVarDecl *getParmVarDecl() const { return PVD; }
106+
107+
void dump(llvm::raw_ostream &OS) const override;
108+
109+
static bool classof(const Loan *L) {
110+
return L->getKind() == Kind::Placeholder;
111+
}
52112
};
53113

54114
/// Manages the creation, storage and retrieval of loans.
55115
class LoanManager {
56116
public:
57117
LoanManager() = default;
58118

59-
Loan &addLoan(AccessPath Path, const Expr *IssueExpr) {
60-
AllLoans.emplace_back(getNextLoanID(), Path, IssueExpr);
61-
return AllLoans.back();
119+
template <typename LoanType, typename... Args>
120+
LoanType *createLoan(Args &&...args) {
121+
static_assert(
122+
std::is_same_v<LoanType, PathLoan> ||
123+
std::is_same_v<LoanType, PlaceholderLoan>,
124+
"createLoan can only be used with PathLoan or PlaceholderLoan");
125+
void *Mem = LoanAllocator.Allocate<LoanType>();
126+
auto *NewLoan =
127+
new (Mem) LoanType(getNextLoanID(), std::forward<Args>(args)...);
128+
AllLoans.push_back(NewLoan);
129+
return NewLoan;
62130
}
63131

64-
const Loan &getLoan(LoanID ID) const {
132+
const Loan *getLoan(LoanID ID) const {
65133
assert(ID.Value < AllLoans.size());
66134
return AllLoans[ID.Value];
67135
}
68-
llvm::ArrayRef<Loan> getLoans() const { return AllLoans; }
136+
llvm::ArrayRef<const Loan *> getLoans() const { return AllLoans; }
69137

70138
private:
71139
LoanID getNextLoanID() { return NextLoanID++; }
72140

73141
LoanID NextLoanID{0};
74142
/// TODO(opt): Profile and evaluate the usefullness of small buffer
75143
/// optimisation.
76-
llvm::SmallVector<Loan> AllLoans;
144+
llvm::SmallVector<const Loan *> AllLoans;
145+
llvm::BumpPtrAllocator LoanAllocator;
77146
};
78147
} // namespace clang::lifetimes::internal
79148

clang/include/clang/Basic/DiagnosticGroups.td

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ def LifetimeSafety : DiagGroup<"experimental-lifetime-safety",
541541
Experimental warnings to detect use-after-free and related temporal safety bugs based on lifetime safety analysis.
542542
}];
543543
}
544+
def LifetimeSafetySuggestions
545+
: DiagGroup<"experimental-lifetime-safety-suggestions">;
544546

545547
def DistributedObjectModifiers : DiagGroup<"distributed-object-modifiers">;
546548
def DllexportExplicitInstantiationDecl : DiagGroup<"dllexport-explicit-instantiation-decl">;

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10784,6 +10784,13 @@ def note_lifetime_safety_used_here : Note<"later used here">;
1078410784
def note_lifetime_safety_destroyed_here : Note<"destroyed here">;
1078510785
def note_lifetime_safety_returned_here : Note<"returned here">;
1078610786

10787+
def warn_lifetime_safety_suggest_lifetimebound
10788+
: Warning<"param should be marked [[clang::lifetimebound]]">,
10789+
InGroup<LifetimeSafetySuggestions>,
10790+
DefaultIgnore;
10791+
10792+
def note_lifetime_safety_suggestion_returned_here : Note<"param returned here">;
10793+
1078710794
// For non-floating point, expressions of the form x == x or x != x
1078810795
// should result in a warning, since these always evaluate to a constant.
1078910796
// Array comparisons have similar warnings

clang/lib/Analysis/LifetimeSafety/Checker.cpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct PendingWarning {
5050
class LifetimeChecker {
5151
private:
5252
llvm::DenseMap<LoanID, PendingWarning> FinalWarningsMap;
53+
llvm::DenseMap<const ParmVarDecl *, const Expr *> AnnotationWarningsMap;
5354
const LoanPropagationAnalysis &LoanPropagation;
5455
const LiveOriginsAnalysis &LiveOrigins;
5556
const FactManager &FactMgr;
@@ -65,7 +66,26 @@ class LifetimeChecker {
6566
for (const Fact *F : FactMgr.getFacts(B))
6667
if (const auto *EF = F->getAs<ExpireFact>())
6768
checkExpiry(EF);
69+
else if (const auto *OEF = F->getAs<OriginEscapesFact>())
70+
checkAnnotations(OEF);
6871
issuePendingWarnings();
72+
suggestAnnotations();
73+
}
74+
75+
/// Checks if an escaping origin holds a placeholder loan, indicating a
76+
/// missing [[clang::lifetimebound]] annotation.
77+
void checkAnnotations(const OriginEscapesFact *OEF) {
78+
OriginID EscapedOID = OEF->getEscapedOriginID();
79+
LoanSet EscapedLoans = LoanPropagation.getLoans(EscapedOID, OEF);
80+
for (LoanID LID : EscapedLoans) {
81+
const Loan *L = FactMgr.getLoanMgr().getLoan(LID);
82+
if (const auto *PL = dyn_cast<PlaceholderLoan>(L)) {
83+
const ParmVarDecl *PVD = PL->getParmVarDecl();
84+
if (PVD->hasAttr<LifetimeBoundAttr>())
85+
continue;
86+
AnnotationWarningsMap.try_emplace(PVD, OEF->getEscapeExpr());
87+
}
88+
}
6989
}
7090

7191
/// Checks for use-after-free & use-after-return errors when a loan expires.
@@ -114,8 +134,9 @@ class LifetimeChecker {
114134
if (!Reporter)
115135
return;
116136
for (const auto &[LID, Warning] : FinalWarningsMap) {
117-
const Loan &L = FactMgr.getLoanMgr().getLoan(LID);
118-
const Expr *IssueExpr = L.IssueExpr;
137+
const Loan *L = FactMgr.getLoanMgr().getLoan(LID);
138+
const auto *BL = cast<PathLoan>(L);
139+
const Expr *IssueExpr = BL->getIssueExpr();
119140
llvm::PointerUnion<const UseFact *, const OriginEscapesFact *>
120141
CausingFact = Warning.CausingFact;
121142
Confidence Confidence = Warning.ConfidenceLevel;
@@ -132,6 +153,13 @@ class LifetimeChecker {
132153
llvm_unreachable("Unhandled CausingFact type");
133154
}
134155
}
156+
157+
void suggestAnnotations() {
158+
if (!Reporter)
159+
return;
160+
for (const auto &[PVD, EscapeExpr] : AnnotationWarningsMap)
161+
Reporter->suggestAnnotation(PVD, EscapeExpr);
162+
}
135163
};
136164
} // namespace
137165

clang/lib/Analysis/LifetimeSafety/Facts.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ void Fact::dump(llvm::raw_ostream &OS, const LoanManager &,
2020
void IssueFact::dump(llvm::raw_ostream &OS, const LoanManager &LM,
2121
const OriginManager &OM) const {
2222
OS << "Issue (";
23-
LM.getLoan(getLoanID()).dump(OS);
23+
LM.getLoan(getLoanID())->dump(OS);
2424
OS << ", ToOrigin: ";
2525
OM.dump(getOriginID(), OS);
2626
OS << ")\n";
@@ -29,7 +29,7 @@ void IssueFact::dump(llvm::raw_ostream &OS, const LoanManager &LM,
2929
void ExpireFact::dump(llvm::raw_ostream &OS, const LoanManager &LM,
3030
const OriginManager &) const {
3131
OS << "Expire (";
32-
LM.getLoan(getLoanID()).dump(OS);
32+
LM.getLoan(getLoanID())->dump(OS);
3333
OS << ")\n";
3434
}
3535

clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,28 @@ static bool hasOrigin(const VarDecl *VD) {
3131
/// This function should be called whenever a DeclRefExpr represents a borrow.
3232
/// \param DRE The declaration reference expression that initiates the borrow.
3333
/// \return The new Loan on success, nullptr otherwise.
34-
static const Loan *createLoan(FactManager &FactMgr, const DeclRefExpr *DRE) {
34+
static const PathLoan *createLoan(FactManager &FactMgr,
35+
const DeclRefExpr *DRE) {
3536
if (const auto *VD = dyn_cast<ValueDecl>(DRE->getDecl())) {
3637
AccessPath Path(VD);
3738
// The loan is created at the location of the DeclRefExpr.
38-
return &FactMgr.getLoanMgr().addLoan(Path, DRE);
39+
return FactMgr.getLoanMgr().createLoan<PathLoan>(Path, DRE);
3940
}
4041
return nullptr;
4142
}
4243

4344
void FactsGenerator::run() {
4445
llvm::TimeTraceScope TimeProfile("FactGenerator");
46+
const CFG &Cfg = *AC.getCFG();
47+
llvm::SmallVector<Fact *> PlaceholderLoanFacts = issuePlaceholderLoans();
4548
// Iterate through the CFG blocks in reverse post-order to ensure that
4649
// initializations and destructions are processed in the correct sequence.
4750
for (const CFGBlock *Block : *AC.getAnalysis<PostOrderCFGView>()) {
4851
CurrentBlockFacts.clear();
4952
EscapesInCurrentBlock.clear();
53+
if (Block == &Cfg.getEntry())
54+
CurrentBlockFacts.append(PlaceholderLoanFacts.begin(),
55+
PlaceholderLoanFacts.end());
5056
for (unsigned I = 0; I < Block->size(); ++I) {
5157
const CFGElement &Element = Block->Elements[I];
5258
if (std::optional<CFGStmt> CS = Element.getAs<CFGStmt>())
@@ -85,7 +91,7 @@ void FactsGenerator::VisitDeclRefExpr(const DeclRefExpr *DRE) {
8591
if (const Loan *L = createLoan(FactMgr, DRE)) {
8692
OriginID ExprOID = FactMgr.getOriginMgr().getOrCreate(*DRE);
8793
CurrentBlockFacts.push_back(
88-
FactMgr.createFact<IssueFact>(L->ID, ExprOID));
94+
FactMgr.createFact<IssueFact>(L->getID(), ExprOID));
8995
}
9096
}
9197
}
@@ -223,13 +229,14 @@ void FactsGenerator::handleLifetimeEnds(const CFGLifetimeEnds &LifetimeEnds) {
223229
if (!LifetimeEndsVD)
224230
return;
225231
// Iterate through all loans to see if any expire.
226-
for (const auto &Loan : FactMgr.getLoanMgr().getLoans()) {
227-
const AccessPath &LoanPath = Loan.Path;
228-
// Check if the loan is for a stack variable and if that variable
229-
// is the one being destructed.
230-
if (LoanPath.D == LifetimeEndsVD)
231-
CurrentBlockFacts.push_back(FactMgr.createFact<ExpireFact>(
232-
Loan.ID, LifetimeEnds.getTriggerStmt()->getEndLoc()));
232+
for (const auto *Loan : FactMgr.getLoanMgr().getLoans()) {
233+
if (const auto *BL = dyn_cast<PathLoan>(Loan)) {
234+
// Check if the loan is for a stack variable and if that variable
235+
// is the one being destructed.
236+
if (BL->getAccessPath().D == LifetimeEndsVD)
237+
CurrentBlockFacts.push_back(FactMgr.createFact<ExpireFact>(
238+
BL->getID(), LifetimeEnds.getTriggerStmt()->getEndLoc()));
239+
}
233240
}
234241
}
235242

@@ -342,4 +349,24 @@ void FactsGenerator::markUseAsWrite(const DeclRefExpr *DRE) {
342349
UseFacts[DRE]->markAsWritten();
343350
}
344351

352+
// Creates an IssueFact for a new placeholder loan for each pointer or reference
353+
// parameter at the function's entry.
354+
llvm::SmallVector<Fact *> FactsGenerator::issuePlaceholderLoans() {
355+
const auto *FD = dyn_cast<FunctionDecl>(AC.getDecl());
356+
if (!FD)
357+
return {};
358+
359+
llvm::SmallVector<Fact *> PlaceholderLoanFacts;
360+
for (const ParmVarDecl *PVD : FD->parameters()) {
361+
if (hasOrigin(PVD)) {
362+
const PlaceholderLoan *L =
363+
FactMgr.getLoanMgr().createLoan<PlaceholderLoan>(PVD);
364+
OriginID OID = FactMgr.getOriginMgr().getOrCreate(*PVD);
365+
PlaceholderLoanFacts.push_back(
366+
FactMgr.createFact<IssueFact>(L->getID(), OID));
367+
}
368+
}
369+
return PlaceholderLoanFacts;
370+
}
371+
345372
} // namespace clang::lifetimes::internal

clang/lib/Analysis/LifetimeSafety/Loans.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010

1111
namespace clang::lifetimes::internal {
1212

13-
void Loan::dump(llvm::raw_ostream &OS) const {
14-
OS << ID << " (Path: ";
13+
void PathLoan::dump(llvm::raw_ostream &OS) const {
14+
OS << getID() << " (Path: ";
1515
OS << Path.D->getNameAsString() << ")";
1616
}
1717

18+
void PlaceholderLoan::dump(llvm::raw_ostream &OS) const {
19+
OS << getID() << " (Placeholder loan)";
20+
}
21+
1822
} // namespace clang::lifetimes::internal

clang/lib/Sema/AnalysisBasedWarnings.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,6 +2883,19 @@ class LifetimeSafetyReporterImpl : public LifetimeSafetyReporter {
28832883
<< EscapeExpr->getEndLoc();
28842884
}
28852885

2886+
void suggestAnnotation(const ParmVarDecl *PVD,
2887+
const Expr *EscapeExpr) override {
2888+
SourceLocation InsertionPoint = Lexer::getLocForEndOfToken(
2889+
PVD->getEndLoc(), 0, S.getSourceManager(), S.getLangOpts());
2890+
S.Diag(PVD->getBeginLoc(), diag::warn_lifetime_safety_suggest_lifetimebound)
2891+
<< PVD->getSourceRange()
2892+
<< FixItHint::CreateInsertion(InsertionPoint,
2893+
" [[clang::lifetimebound]]");
2894+
S.Diag(EscapeExpr->getBeginLoc(),
2895+
diag::note_lifetime_safety_suggestion_returned_here)
2896+
<< EscapeExpr->getSourceRange();
2897+
}
2898+
28862899
private:
28872900
Sema &S;
28882901
};

0 commit comments

Comments
 (0)