[clang] [LifetimeSafety] Support allocating/freeing functions annotated `ownership_takes`/`ownership_returns` (PR #213439)
via cfe-commits
cfe-commits at lists.llvm.org
Sat Aug 1 05:37:28 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-temporal-safety
Author: NeKon69
<details>
<summary>Changes</summary>
This PR extends LifetimeSafety to model allocating/freeing functions annotated with `__attribute__((ownership_returns))` / `__attribute__((ownership_takes))`.
# Contents
* Adds `handleAllocatingCall`, which treats the call result as a fresh allocation and issues a new loan for it (mirroring `new`).
* Adds `handleFreeingCall`, which treats the annotated arguments as freed and invalidates their origins (mirroring `delete`), enabling use-after-free and escape-invalidation diagnostics.
* Extends the `AccessPath` base union to also accept allocating `CallExpr`s, keeping the number of alternatives unchanged. (breaks build on 32-bit systems if 1 more template parameter is added)
Completes part of #<!-- -->213435
Assisted-by: DeepSeek V4 for writing most of the code
---
Full diff: https://github.com/llvm/llvm-project/pull/213439.diff
8 Files Affected:
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+10)
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h (+10)
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h (+12-4)
- (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+51)
- (modified) clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp (+14)
- (modified) clang/lib/Analysis/LifetimeSafety/Loans.cpp (+8-3)
- (modified) clang/lib/Sema/SemaLifetimeSafety.h (+17-3)
- (modified) clang/test/Sema/LifetimeSafety/safety.cpp (+134)
``````````diff
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index 9821078ec1d1e..aabd08d9f68a0 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -117,6 +117,16 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
ArrayRef<const Expr *> Args,
bool IsGslConstruction = false);
+ // Handles calls that allocate memory (e.g. functions annotated with
+ // `ownership_returns`): the call result is a fresh allocation, so a new
+ // loan is issued for it.
+ void handleAllocatingCall(const Expr *Call, const FunctionDecl *FD);
+
+ // Handles calls that free memory (e.g. functions annotated with
+ // `ownership_takes`): the freed arguments' origins are invalidated.
+ void handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
+ ArrayRef<const Expr *> Args);
+
// Detect methods that invalidate iterators/references/pointees.
// For instance methods, Args[0] is the implicit 'this' pointer.
void handleInvalidatingCall(const Expr *Call, const FunctionDecl *FD,
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
index 47fcd5dbfd569..0db1bfd74b4d7 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h
@@ -102,6 +102,16 @@ bool isInvalidationMethod(const CXXMethodDecl &MD);
// (e.g., destructors via implicit 'this', std::destroy_at).
bool destructsFirstArg(const FunctionDecl &FD);
+/// Returns true if the function allocates memory that must be freed by the
+/// caller (e.g. functions annotated with `ownership_returns`, such as
+/// `__attribute__((ownership_returns(malloc)))`).
+bool isAllocatingFunction(const FunctionDecl &FD);
+
+/// Returns true if the function frees memory it takes ownership of (e.g.
+/// functions annotated with `ownership_takes`, such as
+/// `__attribute__((ownership_takes(malloc, 1)))`).
+bool isFreeingFunction(const FunctionDecl &FD);
+
/// Returns true for standard library callable wrappers (e.g., std::function)
/// that can propagate the stored lambda's origins.
bool isStdCallableWrapperType(const CXXRecordDecl *RD);
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
index 8137a207290d7..9007eddeaf4e6 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Loans.h
@@ -111,10 +111,15 @@ class PlaceholderBase : public llvm::FoldingSetNode {
///
/// TODO: Model access paths of other types, e.g. heap and globals.
class AccessPath {
- /// The base of the access path: a variable, temporary, or placeholder.
+ /// The base of the access path: a variable, temporary, placeholder, or an
+ /// allocation expression (a `CXXNewExpr` or an allocating call).
+ ///
+ /// NOTE: Keep the number of alternatives low; PointerUnion packs the
+ /// discriminator in the low bits of the pointer, so more alternatives
+ /// require more alignment than 32-bit builds can always provide.
const llvm::PointerUnion<const clang::ValueDecl *,
const clang::MaterializeTemporaryExpr *,
- const PlaceholderBase *, const clang::CXXNewExpr *>
+ const PlaceholderBase *, const clang::Expr *>
Base;
/// The path elements representing field accesses and access to unnamed
/// interior regions.
@@ -125,6 +130,7 @@ class AccessPath {
AccessPath(const clang::MaterializeTemporaryExpr *MTE) : Base(MTE) {}
AccessPath(const PlaceholderBase *PB) : Base(PB) {}
AccessPath(const clang::CXXNewExpr *New) : Base(New) {}
+ AccessPath(const clang::CallExpr *CE) : Base(CE) {}
/// Creates an extended access path by appending a path element.
/// Example: AccessPath(x_path, field) creates path to `x.field`.
@@ -145,8 +151,10 @@ class AccessPath {
return Base.dyn_cast<const PlaceholderBase *>();
}
- const clang::CXXNewExpr *getAsNewAllocation() const {
- return Base.dyn_cast<const clang::CXXNewExpr *>();
+ /// Returns the allocation expression that created this loan: a
+ /// `CXXNewExpr` or an allocating call.
+ const clang::Expr *getAsAllocation() const {
+ return Base.dyn_cast<const clang::Expr *>();
}
bool operator==(const AccessPath &RHS) const {
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index ac6267dabf48e..1d8eca0d92eae 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -115,6 +115,15 @@ static const Loan *createLoan(FactManager &FactMgr, const CXXNewExpr *NE) {
return FactMgr.getLoanMgr().createLoan(Path, NE);
}
+/// Creates a loan for a heap allocation made by a call (e.g. a function
+/// annotated with `ownership_returns`, like `malloc`).
+/// \param CE The CallExpr that represents the allocation
+/// \return The new Loan on success, nullptr otherwise
+static const Loan *createLoan(FactManager &FactMgr, const CallExpr *CE) {
+ AccessPath Path(CE);
+ return FactMgr.getLoanMgr().createLoan(Path, CE);
+}
+
void FactsGenerator::run() {
llvm::TimeTraceScope TimeProfile("FactGenerator");
const CFG &Cfg = *AC.getCFG();
@@ -1070,6 +1079,46 @@ void FactsGenerator::handleLifetimeCaptureBy(const FunctionDecl *FD,
}
}
+void FactsGenerator::handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
+ ArrayRef<const Expr *> Args) {
+ // FIXME: `ParamIdx` accounts for the implicit 'this' parameter of
+ // constructors, but VisitCXXConstructExpr passes only the explicit
+ // arguments, so the index mapping would be off by one.
+ if (isa<CXXConstructorDecl>(FD))
+ return;
+ for (const OwnershipAttr *Attr : FD->specific_attrs<OwnershipAttr>()) {
+ if (Attr->getOwnKind() != OwnershipAttr::Takes)
+ continue;
+ for (const ParamIdx &Idx : Attr->args()) {
+ // `getLLVMIndex` encodes zero-origin indices including any implicit
+ // 'this' parameter, matching the layout of the Args array.
+ unsigned ArgIndex = Idx.getLLVMIndex();
+ if (ArgIndex >= Args.size())
+ continue;
+ OriginList *ArgList = getOriginsList(*Args[ArgIndex]);
+ if (!ArgList)
+ continue;
+ CurrentBlockFacts.push_back(FactMgr.createFact<InvalidateOriginFact>(
+ ArgList->getOuterOriginID(), Call));
+ }
+ }
+}
+
+void FactsGenerator::handleAllocatingCall(const Expr *Call,
+ const FunctionDecl *FD) {
+ if (!isAllocatingFunction(*FD))
+ return;
+ const auto *CE = dyn_cast<CallExpr>(Call);
+ if (!CE)
+ return;
+ OriginList *CallList = getOriginsList(*Call);
+ if (!CallList)
+ return;
+ const Loan *L = createLoan(FactMgr, CE);
+ CurrentBlockFacts.push_back(
+ FactMgr.createFact<IssueFact>(L->getID(), CallList->getOuterOriginID()));
+}
+
void FactsGenerator::handleFunctionCall(const Expr *Call,
const FunctionDecl *FD,
ArrayRef<const Expr *> Args,
@@ -1087,6 +1136,8 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
handleMovedArgsInCall(FD, Args);
handleImplicitObjectFieldUses(Call, FD);
handleLifetimeCaptureBy(FD, Args);
+ handleAllocatingCall(Call, FD);
+ handleFreeingCall(Call, FD, Args);
if (!CallList)
return;
if (isStdReferenceCast(FD)) {
diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
index 6a52616c5d590..62f178618701b 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
@@ -453,6 +453,20 @@ bool destructsFirstArg(const FunctionDecl &FD) {
return isInStlNamespace(&FD) && getName(FD) == "destroy_at";
}
+bool isAllocatingFunction(const FunctionDecl &FD) {
+ return llvm::any_of(FD.specific_attrs<OwnershipAttr>(),
+ [](const OwnershipAttr *Attr) {
+ return Attr->getOwnKind() == OwnershipAttr::Returns;
+ });
+}
+
+bool isFreeingFunction(const FunctionDecl &FD) {
+ return llvm::any_of(FD.specific_attrs<OwnershipAttr>(),
+ [](const OwnershipAttr *Attr) {
+ return Attr->getOwnKind() == OwnershipAttr::Takes;
+ });
+}
+
bool isStdCallableWrapperType(const CXXRecordDecl *RD) {
if (!RD || !isInStlNamespace(RD))
return false;
diff --git a/clang/lib/Analysis/LifetimeSafety/Loans.cpp b/clang/lib/Analysis/LifetimeSafety/Loans.cpp
index e71842eefca6a..3974e04c0e7c4 100644
--- a/clang/lib/Analysis/LifetimeSafety/Loans.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Loans.cpp
@@ -21,9 +21,14 @@ void AccessPath::dump(llvm::raw_ostream &OS) const {
OS << "$" << PVD->getNameAsString();
else if (PB->getImplicitThisParent())
OS << "$this";
- } else if (const auto *E = getAsNewAllocation())
- OS << "NewAllocation at " << E;
- else
+ } else if (const auto *E = getAsAllocation()) {
+ if (isa<CXXNewExpr>(E))
+ OS << "NewAllocation at " << E;
+ else if (isa<CallExpr>(E))
+ OS << "HeapAllocation at " << E;
+ else
+ llvm_unreachable("unexpected allocation expression");
+ } else
llvm_unreachable("access path base invalid");
for (const auto &E : Elements)
E.dump(OS);
diff --git a/clang/lib/Sema/SemaLifetimeSafety.h b/clang/lib/Sema/SemaLifetimeSafety.h
index 1d9f94be7e22d..762e9e336fab9 100644
--- a/clang/lib/Sema/SemaLifetimeSafety.h
+++ b/clang/lib/Sema/SemaLifetimeSafety.h
@@ -192,7 +192,7 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
reportUseAfterInvalidation(const Expr *IssueExpr, const Expr *UseExpr,
const Expr *InvalidationExpr,
llvm::ArrayRef<const Expr *> ExprChain) override {
- auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ auto WarnDiag = isFreeingExpr(InvalidationExpr)
? diag::warn_lifetime_safety_use_after_free
: diag::warn_lifetime_safety_invalidation;
std::string InvalidatedSubject = getDiagSubjectDescription(IssueExpr);
@@ -208,7 +208,7 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
const Expr *InvalidationExpr,
llvm::ArrayRef<const Expr *> ExprChain) override {
- auto WarnDiag = isa<CXXDeleteExpr>(InvalidationExpr)
+ auto WarnDiag = isFreeingExpr(InvalidationExpr)
? diag::warn_lifetime_safety_use_after_free
: diag::warn_lifetime_safety_invalidation;
std::string InvalidatedSubject = getDiagSubjectDescription(PVD);
@@ -459,6 +459,18 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
}
private:
+ // Returns true if the expression frees memory: a `delete` expression or a
+ // call to a freeing function (e.g. annotated with `ownership_takes`).
+ static bool isFreeingExpr(const Expr *InvalidationExpr) {
+ if (isa<CXXDeleteExpr>(InvalidationExpr))
+ return true;
+ const auto *CE = dyn_cast<CallExpr>(InvalidationExpr);
+ if (!CE)
+ return false;
+ const FunctionDecl *FD = CE->getDirectCallee();
+ return FD && isFreeingFunction(*FD);
+ }
+
struct LifetimeBoundMacroCache {
bool IsBuilt = false;
SmallVector<const IdentifierInfo *> Candidates;
@@ -531,7 +543,7 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
void reportInvalidationSite(const Expr *InvalidationExpr,
StringRef InvalidatedSubject) {
- auto Diag = isa<CXXDeleteExpr>(InvalidationExpr)
+ auto Diag = isFreeingExpr(InvalidationExpr)
? diag::note_lifetime_safety_freed_here
: diag::note_lifetime_safety_invalidated_here;
S.Diag(InvalidationExpr->getExprLoc(), Diag)
@@ -656,6 +668,8 @@ class LifetimeSafetySemaHelperImpl : public LifetimeSafetySemaHelper {
llvm::raw_string_ostream OS(Name);
FD->getNameForDiagnostic(OS, S.getPrintingPolicy(),
/*Qualified=*/false);
+ if (isAllocatingFunction(*FD))
+ return "object allocated by '" + Name + "'";
return "result of call to '" + Name + "'";
}
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index f3b042cce74b2..f52a3a0710914 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -3379,6 +3379,140 @@ void allocate_void_ptr() {
} // namespace new_allocation
+namespace ownership_functions {
+
+// Allocating/freeing functions are annotated with the ownership attributes:
+// * ownership_returns: the result is a fresh allocation with a new loan.
+// * ownership_takes: the annotated arguments are freed, invalidating their
+// origins.
+
+__attribute__((ownership_returns(malloc))) int *myalloc(void);
+__attribute__((ownership_takes(malloc, 1))) void myfree(int *p);
+__attribute__((ownership_takes(malloc, 1, 2))) void myfree2(int *a, int *b);
+__attribute__((ownership_returns(malloc), ownership_takes(malloc, 1))) int *
+myrealloc(int *p);
+
+int *g_owned_ptr; // expected-note {{this global dangles}}
+
+void ownership_returns_basic_uaf() {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+ *p = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_free_no_use() {
+ int *p = myalloc();
+ myfree(p);
+}
+
+void ownership_returns_free_stack() {
+ int local = 42;
+ int *p = &local; // expected-warning {{local variable 'local' does not live long enough}}
+ myfree(&local); // expected-note {{local variable 'local' is freed here}}
+ (void)*p; // expected-note {{later used here}}
+}
+
+void ownership_returns_escape_global() {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' escapes to the global variable 'g_owned_ptr' and is later invalidated}}
+ g_owned_ptr = p;
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+}
+
+void ownership_returns_global_alias_uaf() {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ g_owned_ptr = p; // expected-note {{local variable 'p' aliases the storage of object allocated by 'myalloc'}}
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+ *g_owned_ptr = 1; // expected-note {{later used here}}
+ g_owned_ptr = nullptr;
+}
+
+struct OwnershipFieldHolder {
+ int *f; // expected-note {{this field dangles}}
+ void go() {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' escapes to the field 'f' and is later invalidated}}
+ this->f = p;
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+ }
+};
+
+void ownership_returns_heap_escape_ok() {
+ int *p = myalloc();
+ g_owned_ptr = p;
+ myfree(p);
+ g_owned_ptr = nullptr;
+}
+
+void ownership_returns_free_param(int *p) { // expected-warning {{parameter 'p' does not live long enough}}
+ myfree(p); // expected-note {{parameter 'p' is freed here}}
+ *p = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_multi_arg_free() {
+ int *a = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ int *b = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ myfree2(a, b); // expected-note 2 {{object allocated by 'myalloc' is freed here}}
+ *a = 1; // expected-note {{later used here}}
+ *b = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_realloc() {
+ int *p = myalloc();
+ p = myrealloc(p); // expected-warning {{object allocated by 'myrealloc' does not live long enough}}
+ *p = 1;
+ myfree(p); // expected-note {{object allocated by 'myrealloc' is freed here}}
+ *p = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_free_then_reassign() {
+ int *p = myalloc();
+ myfree(p);
+ p = myalloc();
+ *p = 1;
+ myfree(p);
+}
+
+void ownership_returns_conditional_free(int k) {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ if (k)
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+ else
+ myfree(p);
+ *p = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_double_free() {
+ int *p = myalloc(); // expected-warning {{object allocated by 'myalloc' does not live long enough}}
+ myfree(p); // expected-note {{object allocated by 'myalloc' is freed here}}
+ myfree(p); // expected-note {{later used here}}
+}
+
+struct OwnershipAllocator {
+ __attribute__((ownership_returns(malloc))) int *alloc(void);
+ // The index accounts for the implicit 'this' parameter, so 2 is the first
+ // explicit parameter.
+ __attribute__((ownership_takes(malloc, 2))) void dealloc(int *p);
+ __attribute__((ownership_returns(malloc), ownership_takes(malloc, 2))) int *
+ realloc2(int *p);
+};
+
+void ownership_returns_member_allocator() {
+ OwnershipAllocator a;
+ int *p = a.alloc(); // expected-warning {{object allocated by 'alloc' does not live long enough}}
+ a.dealloc(p); // expected-note {{object allocated by 'alloc' is freed here}}
+ *p = 1; // expected-note {{later used here}}
+}
+
+void ownership_returns_member_realloc() {
+ OwnershipAllocator a;
+ int *p = a.alloc();
+ p = a.realloc2(p); // expected-warning {{object allocated by 'realloc2' does not live long enough}}
+ *p = 1;
+ a.dealloc(p); // expected-note {{object allocated by 'realloc2' is freed here}}
+ *p = 1; // expected-note {{later used here}}
+}
+
+} // namespace ownership_functions
+
namespace placement_new {
void placement_new_int_basic() {
``````````
</details>
https://github.com/llvm/llvm-project/pull/213439
More information about the cfe-commits
mailing list