https://github.com/NeKon69 created 
https://github.com/llvm/llvm-project/pull/213439

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

>From df8c73b63425ab109ec677793bcf0cd80cf2b303 Mon Sep 17 00:00:00 2001
From: NeKon69 <[email protected]>
Date: Sat, 1 Aug 2026 15:06:50 +0300
Subject: [PATCH] [LifetimeSafety] Support allocating/freeing functions
 annotated with ownership_takes/ownership_returns

---
 .../Analyses/LifetimeSafety/FactsGenerator.h  |  10 ++
 .../LifetimeSafety/LifetimeAnnotations.h      |  10 ++
 .../Analysis/Analyses/LifetimeSafety/Loans.h  |  16 ++-
 .../LifetimeSafety/FactsGenerator.cpp         |  51 +++++++
 .../LifetimeSafety/LifetimeAnnotations.cpp    |  14 ++
 clang/lib/Analysis/LifetimeSafety/Loans.cpp   |  11 +-
 clang/lib/Sema/SemaLifetimeSafety.h           |  20 ++-
 clang/test/Sema/LifetimeSafety/safety.cpp     | 134 ++++++++++++++++++
 8 files changed, 256 insertions(+), 10 deletions(-)

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() {

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to