[llvm-branch-commits] [clang] [LifetimeSafety] Support allocating/freeing builtins (PR #213443)

via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Aug 3 08:07:30 PDT 2026


https://github.com/NeKon69 updated https://github.com/llvm/llvm-project/pull/213443

>From 1be2288960563eae3a7c4bc3b6cb80a8273db257 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 1 Aug 2026 15:52:51 +0300
Subject: [PATCH 1/3] [LifetimeSafety] Support allocating/freeing builtins

---
 .../LifetimeSafety/FactsGenerator.cpp         | 29 ++++++++++++++-----
 .../LifetimeSafety/LifetimeAnnotations.cpp    | 20 +++++++++++++
 .../LifetimeSafety/Inputs/lifetime-analysis.h |  5 ++++
 clang/test/Sema/LifetimeSafety/safety.cpp     | 26 +++++++++++++++--
 4 files changed, 70 insertions(+), 10 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 1d8eca0d92eae..a91673c0bc5dc 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -1086,22 +1086,35 @@ void FactsGenerator::handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
   // arguments, so the index mapping would be off by one.
   if (isa<CXXConstructorDecl>(FD))
     return;
+  if (!isFreeingFunction(*FD))
+    return;
+  auto InvalidateArg = [&](unsigned ArgIndex) {
+    if (ArgIndex >= Args.size())
+      return;
+    OriginList *ArgList = getOriginsList(*Args[ArgIndex]);
+    if (!ArgList)
+      return;
+    CurrentBlockFacts.push_back(FactMgr.createFact<InvalidateOriginFact>(
+        ArgList->getOuterOriginID(), Call));
+  };
+
+  bool HasAttr = false;
   for (const OwnershipAttr *Attr : FD->specific_attrs<OwnershipAttr>()) {
     if (Attr->getOwnKind() != OwnershipAttr::Takes)
       continue;
+    HasAttr = true;
     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));
+      InvalidateArg(Idx.getLLVMIndex());
     }
   }
+  // Allocating/freeing builtins (e.g. `free`, `realloc`) free their first
+  // argument.
+  if (!HasAttr) {
+    assert(!Args.empty() && "freeing builtins take at least one argument");
+    InvalidateArg(0);
+  }
 }
 
 void FactsGenerator::handleAllocatingCall(const Expr *Call,
diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
index 62f178618701b..fa8beda3de745 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeAnnotations.cpp
@@ -454,6 +454,17 @@ bool destructsFirstArg(const FunctionDecl &FD) {
 }
 
 bool isAllocatingFunction(const FunctionDecl &FD) {
+  switch (FD.getBuiltinID()) {
+  case Builtin::BImalloc:
+  case Builtin::BI__builtin_malloc:
+  case Builtin::BIcalloc:
+  case Builtin::BI__builtin_calloc:
+  case Builtin::BIrealloc:
+  case Builtin::BI__builtin_realloc:
+    return true;
+  default:
+    break;
+  }
   return llvm::any_of(FD.specific_attrs<OwnershipAttr>(),
                       [](const OwnershipAttr *Attr) {
                         return Attr->getOwnKind() == OwnershipAttr::Returns;
@@ -461,6 +472,15 @@ bool isAllocatingFunction(const FunctionDecl &FD) {
 }
 
 bool isFreeingFunction(const FunctionDecl &FD) {
+  switch (FD.getBuiltinID()) {
+  case Builtin::BIfree:
+  case Builtin::BI__builtin_free:
+  case Builtin::BIrealloc:
+  case Builtin::BI__builtin_realloc:
+    return true;
+  default:
+    break;
+  }
   return llvm::any_of(FD.specific_attrs<OwnershipAttr>(),
                       [](const OwnershipAttr *Attr) {
                         return Attr->getOwnKind() == OwnershipAttr::Takes;
diff --git a/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h b/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
index 024c3c2bc51b7..146756db6985a 100644
--- a/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
+++ b/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
@@ -362,3 +362,8 @@ void *operator new[](std::size_t, void *) noexcept;
 void *operator new(std::size_t, const std::nothrow_t &) noexcept;
 void *operator new(std::size_t, std::align_val_t,
                    const std::nothrow_t &) noexcept;
+
+extern "C" void *malloc(unsigned long);
+extern "C" void free(void *);
+extern "C" void *calloc(unsigned long, unsigned long);
+extern "C" void *realloc(void *, unsigned long);
diff --git a/clang/test/Sema/LifetimeSafety/safety.cpp b/clang/test/Sema/LifetimeSafety/safety.cpp
index f52a3a0710914..f68de8f83006d 100644
--- a/clang/test/Sema/LifetimeSafety/safety.cpp
+++ b/clang/test/Sema/LifetimeSafety/safety.cpp
@@ -3379,12 +3379,14 @@ void allocate_void_ptr() {
 
 } // namespace new_allocation
 
-namespace ownership_functions {
+namespace allocating_freeing_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.
+// Plain malloc/free-style builtins are modeled the same way without any
+// attributes.
 
 __attribute__((ownership_returns(malloc))) int *myalloc(void);
 __attribute__((ownership_takes(malloc, 1))) void myfree(int *p);
@@ -3511,7 +3513,27 @@ void ownership_returns_member_realloc() {
   *p = 1;       // expected-note {{later used here}}
 }
 
-} // namespace ownership_functions
+void builtin_malloc_free_uaf() {
+  int *p = (int *)malloc(sizeof(int)); // expected-warning {{object allocated by 'malloc' does not live long enough}}
+  free(p);                             // expected-note {{object allocated by 'malloc' is freed here}}
+  *p = 1;                              // expected-note {{later used here}}
+}
+
+void builtin_malloc_prefixed_uaf() {
+  int *p = (int *)__builtin_malloc(sizeof(int)); // expected-warning {{object allocated by '__builtin_malloc' does not live long enough}}
+  __builtin_free(p);                             // expected-note {{object allocated by '__builtin_malloc' is freed here}}
+  *p = 1;                                        // expected-note {{later used here}}
+}
+
+void builtin_realloc() {
+  int *p = (int *)malloc(sizeof(int));
+  p = (int *)realloc(p, sizeof(int) * 2); // expected-warning {{object allocated by 'realloc' does not live long enough}}
+  *p = 1;
+  free(p); // expected-note {{object allocated by 'realloc' is freed here}}
+  *p = 1;  // expected-note {{later used here}}
+}
+
+} // namespace allocating_freeing_functions
 
 namespace placement_new {
 

>From cfad953ff5527c61002317dd80a89a8e762bdf98 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Sat, 1 Aug 2026 17:28:27 +0300
Subject: [PATCH 2/3] fix test

---
 clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h b/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
index 146756db6985a..78582402981d2 100644
--- a/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
+++ b/clang/test/Sema/LifetimeSafety/Inputs/lifetime-analysis.h
@@ -363,7 +363,7 @@ void *operator new(std::size_t, const std::nothrow_t &) noexcept;
 void *operator new(std::size_t, std::align_val_t,
                    const std::nothrow_t &) noexcept;
 
-extern "C" void *malloc(unsigned long);
+extern "C" void *malloc(std::size_t);
 extern "C" void free(void *);
-extern "C" void *calloc(unsigned long, unsigned long);
-extern "C" void *realloc(void *, unsigned long);
+extern "C" void *calloc(std::size_t, std::size_t);
+extern "C" void *realloc(void *, std::size_t);

>From 501aa4417e8579a2f9161fce93a10c759968aae0 Mon Sep 17 00:00:00 2001
From: NeKon69 <nobodqwe at gmail.com>
Date: Mon, 3 Aug 2026 11:20:21 +0300
Subject: [PATCH 3/3] cleanup

---
 clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index a91673c0bc5dc..dab3a1ae21574 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -1089,8 +1089,8 @@ void FactsGenerator::handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
   if (!isFreeingFunction(*FD))
     return;
   auto InvalidateArg = [&](unsigned ArgIndex) {
-    if (ArgIndex >= Args.size())
-      return;
+    assert(ArgIndex < Args.size() && "Arg index should be in bounds");
+
     OriginList *ArgList = getOriginsList(*Args[ArgIndex]);
     if (!ArgList)
       return;
@@ -1098,11 +1098,11 @@ void FactsGenerator::handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
         ArgList->getOuterOriginID(), Call));
   };
 
-  bool HasAttr = false;
+  bool HasOwnershipTakesAttr = false;
   for (const OwnershipAttr *Attr : FD->specific_attrs<OwnershipAttr>()) {
     if (Attr->getOwnKind() != OwnershipAttr::Takes)
       continue;
-    HasAttr = true;
+    HasOwnershipTakesAttr = true;
     for (const ParamIdx &Idx : Attr->args()) {
       // `getLLVMIndex` encodes zero-origin indices including any implicit
       // 'this' parameter, matching the layout of the Args array.
@@ -1111,7 +1111,7 @@ void FactsGenerator::handleFreeingCall(const Expr *Call, const FunctionDecl *FD,
   }
   // Allocating/freeing builtins (e.g. `free`, `realloc`) free their first
   // argument.
-  if (!HasAttr) {
+  if (!HasOwnershipTakesAttr) {
     assert(!Args.empty() && "freeing builtins take at least one argument");
     InvalidateArg(0);
   }



More information about the llvm-branch-commits mailing list