[clang] [Clang SA]: add support for mismatched ownership_returns+ownership_takes calls for custom allocation classes (PR #98941)

Pavel Skripkin via cfe-commits cfe-commits at lists.llvm.org
Mon Jul 15 10:46:47 PDT 2024


https://github.com/pskrgag created https://github.com/llvm/llvm-project/pull/98941

Add support for checking mismatched ownership_returns/ownership_takes attributes.

Closes #76861

>From e08dd7946051202d35199bf181602f88589f8ed9 Mon Sep 17 00:00:00 2001
From: Pavel Skripkin <paskripkin at gmail.com>
Date: Sun, 14 Jul 2024 01:24:20 +0300
Subject: [PATCH 1/3] csa/MallocChecker: add support for custom allocation
 classes

Add support for custom allocation classes that could be specified by
ownership_returns attribute.

This patch adds basic support, so new class is not contructed anywhere
and handled as an error everywhere.
---
 .../StaticAnalyzer/Checkers/MallocChecker.cpp | 184 ++++++++++++------
 1 file changed, 122 insertions(+), 62 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
index fe202c79ed620..9d170b6f424d3 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
@@ -103,14 +103,46 @@ using namespace std::placeholders;
 namespace {
 
 // Used to check correspondence between allocators and deallocators.
-enum AllocationFamily {
+enum AllocationFamilyKind {
   AF_None,
   AF_Malloc,
   AF_CXXNew,
   AF_CXXNewArray,
   AF_IfNameIndex,
   AF_Alloca,
-  AF_InnerBuffer
+  AF_InnerBuffer,
+  AF_Custom,
+};
+
+class AllocationFamily {
+public:
+  AllocationFamily(AllocationFamilyKind kind,
+                   std::optional<StringRef> name = std::nullopt)
+      : _kind(kind), _customName(name) {
+    assert(kind != AF_Custom || name != std::nullopt);
+  }
+
+  bool operator==(const AllocationFamily &Other) const {
+    if (Other.kind() == this->kind()) {
+      return this->kind() == AF_Custom ? this->_customName == Other._customName
+                                       : true;
+    } else {
+      return false;
+    }
+  }
+
+  bool operator==(const AllocationFamilyKind &kind) {
+    return this->kind() == kind;
+  }
+
+  bool operator!=(const AllocationFamilyKind &kind) { return !(*this == kind); }
+
+  std::optional<StringRef> name() const { return _customName; }
+  AllocationFamilyKind kind() const { return _kind; }
+
+private:
+  AllocationFamilyKind _kind;
+  std::optional<StringRef> _customName;
 };
 
 } // end of anonymous namespace
@@ -194,7 +226,7 @@ class RefState {
   void Profile(llvm::FoldingSetNodeID &ID) const {
     ID.AddInteger(K);
     ID.AddPointer(S);
-    ID.AddInteger(Family);
+    ID.AddInteger(Family.kind());
   }
 
   LLVM_DUMP_METHOD void dump(raw_ostream &OS) const {
@@ -1918,26 +1950,52 @@ static bool printMemFnName(raw_ostream &os, CheckerContext &C, const Expr *E) {
 
 static void printExpectedAllocName(raw_ostream &os, AllocationFamily Family) {
 
-  switch(Family) {
-    case AF_Malloc: os << "malloc()"; return;
-    case AF_CXXNew: os << "'new'"; return;
-    case AF_CXXNewArray: os << "'new[]'"; return;
-    case AF_IfNameIndex: os << "'if_nameindex()'"; return;
-    case AF_InnerBuffer: os << "container-specific allocator"; return;
-    case AF_Alloca:
-    case AF_None: llvm_unreachable("not a deallocation expression");
+  switch (Family.kind()) {
+  case AF_Malloc:
+    os << "malloc()";
+    return;
+  case AF_CXXNew:
+    os << "'new'";
+    return;
+  case AF_CXXNewArray:
+    os << "'new[]'";
+    return;
+  case AF_IfNameIndex:
+    os << "'if_nameindex()'";
+    return;
+  case AF_InnerBuffer:
+    os << "container-specific allocator";
+    return;
+  case AF_Alloca:
+  case AF_None:
+    llvm_unreachable("not a deallocation expression");
+  case AF_Custom:
+    llvm_unreachable("not a deallocation expression");
   }
 }
 
 static void printExpectedDeallocName(raw_ostream &os, AllocationFamily Family) {
-  switch(Family) {
-    case AF_Malloc: os << "free()"; return;
-    case AF_CXXNew: os << "'delete'"; return;
-    case AF_CXXNewArray: os << "'delete[]'"; return;
-    case AF_IfNameIndex: os << "'if_freenameindex()'"; return;
-    case AF_InnerBuffer: os << "container-specific deallocator"; return;
-    case AF_Alloca:
-    case AF_None: llvm_unreachable("suspicious argument");
+  switch (Family.kind()) {
+  case AF_Malloc:
+    os << "free()";
+    return;
+  case AF_CXXNew:
+    os << "'delete'";
+    return;
+  case AF_CXXNewArray:
+    os << "'delete[]'";
+    return;
+  case AF_IfNameIndex:
+    os << "'if_freenameindex()'";
+    return;
+  case AF_InnerBuffer:
+    os << "container-specific deallocator";
+    return;
+  case AF_Alloca:
+  case AF_None:
+    llvm_unreachable("suspicious argument");
+  case AF_Custom:
+    llvm_unreachable("suspicious argument");
   }
 }
 
@@ -2119,7 +2177,7 @@ MallocChecker::FreeMemAux(CheckerContext &C, const Expr *ArgExpr,
 std::optional<MallocChecker::CheckKind>
 MallocChecker::getCheckIfTracked(AllocationFamily Family,
                                  bool IsALeakCheck) const {
-  switch (Family) {
+  switch (Family.kind()) {
   case AF_Malloc:
   case AF_Alloca:
   case AF_IfNameIndex: {
@@ -2144,6 +2202,7 @@ MallocChecker::getCheckIfTracked(AllocationFamily Family,
       return CK_InnerPointerChecker;
     return std::nullopt;
   }
+  case AF_Custom:
   case AF_None: {
     llvm_unreachable("no family");
   }
@@ -3483,53 +3542,54 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
           Sym, "Returned allocated memory");
     } else if (isReleased(RSCurr, RSPrev, S)) {
       const auto Family = RSCurr->getAllocationFamily();
-      switch (Family) {
-        case AF_Alloca:
-        case AF_Malloc:
-        case AF_CXXNew:
-        case AF_CXXNewArray:
-        case AF_IfNameIndex:
-          Msg = "Memory is released";
+      switch (Family.kind()) {
+      case AF_Alloca:
+      case AF_Malloc:
+      case AF_CXXNew:
+      case AF_CXXNewArray:
+      case AF_IfNameIndex:
+        Msg = "Memory is released";
+        StackHint = std::make_unique<StackHintGeneratorForSymbol>(
+            Sym, "Returning; memory was released");
+        break;
+      case AF_InnerBuffer: {
+        const MemRegion *ObjRegion =
+            allocation_state::getContainerObjRegion(statePrev, Sym);
+        const auto *TypedRegion = cast<TypedValueRegion>(ObjRegion);
+        QualType ObjTy = TypedRegion->getValueType();
+        OS << "Inner buffer of '" << ObjTy << "' ";
+
+        if (N->getLocation().getKind() == ProgramPoint::PostImplicitCallKind) {
+          OS << "deallocated by call to destructor";
           StackHint = std::make_unique<StackHintGeneratorForSymbol>(
-              Sym, "Returning; memory was released");
-          break;
-        case AF_InnerBuffer: {
-          const MemRegion *ObjRegion =
-              allocation_state::getContainerObjRegion(statePrev, Sym);
-          const auto *TypedRegion = cast<TypedValueRegion>(ObjRegion);
-          QualType ObjTy = TypedRegion->getValueType();
-          OS << "Inner buffer of '" << ObjTy << "' ";
-
-          if (N->getLocation().getKind() == ProgramPoint::PostImplicitCallKind) {
-            OS << "deallocated by call to destructor";
-            StackHint = std::make_unique<StackHintGeneratorForSymbol>(
-                Sym, "Returning; inner buffer was deallocated");
-          } else {
-            OS << "reallocated by call to '";
-            const Stmt *S = RSCurr->getStmt();
-            if (const auto *MemCallE = dyn_cast<CXXMemberCallExpr>(S)) {
-              OS << MemCallE->getMethodDecl()->getDeclName();
-            } else if (const auto *OpCallE = dyn_cast<CXXOperatorCallExpr>(S)) {
-              OS << OpCallE->getDirectCallee()->getDeclName();
-            } else if (const auto *CallE = dyn_cast<CallExpr>(S)) {
-              auto &CEMgr = BRC.getStateManager().getCallEventManager();
-              CallEventRef<> Call =
-                  CEMgr.getSimpleCall(CallE, state, CurrentLC, {nullptr, 0});
-              if (const auto *D = dyn_cast_or_null<NamedDecl>(Call->getDecl()))
-                OS << D->getDeclName();
-              else
-                OS << "unknown";
-            }
-            OS << "'";
-            StackHint = std::make_unique<StackHintGeneratorForSymbol>(
-                Sym, "Returning; inner buffer was reallocated");
+              Sym, "Returning; inner buffer was deallocated");
+        } else {
+          OS << "reallocated by call to '";
+          const Stmt *S = RSCurr->getStmt();
+          if (const auto *MemCallE = dyn_cast<CXXMemberCallExpr>(S)) {
+            OS << MemCallE->getMethodDecl()->getDeclName();
+          } else if (const auto *OpCallE = dyn_cast<CXXOperatorCallExpr>(S)) {
+            OS << OpCallE->getDirectCallee()->getDeclName();
+          } else if (const auto *CallE = dyn_cast<CallExpr>(S)) {
+            auto &CEMgr = BRC.getStateManager().getCallEventManager();
+            CallEventRef<> Call =
+                CEMgr.getSimpleCall(CallE, state, CurrentLC, {nullptr, 0});
+            if (const auto *D = dyn_cast_or_null<NamedDecl>(Call->getDecl()))
+              OS << D->getDeclName();
+            else
+              OS << "unknown";
           }
-          Msg = OS.str();
-          break;
+          OS << "'";
+          StackHint = std::make_unique<StackHintGeneratorForSymbol>(
+              Sym, "Returning; inner buffer was reallocated");
+        }
+        Msg = OS.str();
+        break;
         }
+        case AF_Custom:
         case AF_None:
           llvm_unreachable("Unhandled allocation family!");
-      }
+        }
 
       // See if we're releasing memory while inlining a destructor
       // (or one of its callees). This turns on various common

>From 4655163bdde2decf8c5a7db08c01e51b2574c6d8 Mon Sep 17 00:00:00 2001
From: Pavel Skripkin <paskripkin at gmail.com>
Date: Mon, 15 Jul 2024 00:54:31 +0300
Subject: [PATCH 2/3] csa/MismatchedDeallocator: handle custom allocation
 clasess

Patch introduces support for custom allocation classes in
MismatchedDeallocator.

Patch preserves previous behavior, in which 'malloc' class was handled
as AF_Malloc type.
---
 .../StaticAnalyzer/Checkers/MallocChecker.cpp | 70 ++++++++++++++-----
 1 file changed, 53 insertions(+), 17 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
index 9d170b6f424d3..821e0c8da5d2d 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
@@ -1702,15 +1702,18 @@ MallocChecker::MallocMemReturnsAttr(CheckerContext &C, const CallEvent &Call,
   if (!State)
     return nullptr;
 
-  if (Att->getModule()->getName() != "malloc")
-    return nullptr;
+  // Preseve previous behavior when "malloc" class means AF_Malloc
+  auto attrClassName = Att->getModule()->getName();
+  auto AF = attrClassName == "malloc"
+                ? AF_Malloc
+                : AllocationFamily(AF_Custom, attrClassName);
 
   if (!Att->args().empty()) {
     return MallocMemAux(C, Call,
                         Call.getArgExpr(Att->args_begin()->getASTIndex()),
-                        UndefinedVal(), State, AF_Malloc);
+                        UndefinedVal(), State, AF);
   }
-  return MallocMemAux(C, Call, UnknownVal(), UndefinedVal(), State, AF_Malloc);
+  return MallocMemAux(C, Call, UnknownVal(), UndefinedVal(), State, AF);
 }
 
 ProgramStateRef MallocChecker::MallocMemAux(CheckerContext &C,
@@ -1862,16 +1865,18 @@ ProgramStateRef MallocChecker::FreeMemAttr(CheckerContext &C,
   if (!State)
     return nullptr;
 
-  if (Att->getModule()->getName() != "malloc")
-    return nullptr;
+  // Preseve previous behavior when "malloc" class means AF_Malloc
+  auto attrClassName = Att->getModule()->getName();
+  auto AF = attrClassName == "malloc"
+                ? AF_Malloc
+                : AllocationFamily(AF_Custom, attrClassName);
 
   bool IsKnownToBeAllocated = false;
 
   for (const auto &Arg : Att->args()) {
-    ProgramStateRef StateI =
-        FreeMemAux(C, Call, State, Arg.getASTIndex(),
-                   Att->getOwnKind() == OwnershipAttr::Holds,
-                   IsKnownToBeAllocated, AF_Malloc);
+    ProgramStateRef StateI = FreeMemAux(
+        C, Call, State, Arg.getASTIndex(),
+        Att->getOwnKind() == OwnershipAttr::Holds, IsKnownToBeAllocated, AF);
     if (StateI)
       State = StateI;
   }
@@ -1909,6 +1914,33 @@ static bool didPreviousFreeFail(ProgramStateRef State,
   return false;
 }
 
+static void printOwnershipTakesList(raw_ostream &os, CheckerContext &C,
+                                    const Expr *E) {
+  if (const CallExpr *CE = dyn_cast<CallExpr>(E)) {
+    const FunctionDecl *FD = CE->getDirectCallee();
+    if (!FD)
+      return;
+
+    if (FD->hasAttrs()) {
+      bool attrPrinted = false;
+
+      for (const auto *I : FD->specific_attrs<OwnershipAttr>()) {
+        OwnershipAttr::OwnershipKind OwnKind = I->getOwnKind();
+
+        if (OwnKind == OwnershipAttr::Takes) {
+          if (attrPrinted)
+            os << ", ";
+          else
+            os << ", which takes ownership of ";
+
+          os << '\'' << I->getModule()->getName() << "'";
+          attrPrinted = true;
+        }
+      }
+    }
+  }
+}
+
 static bool printMemFnName(raw_ostream &os, CheckerContext &C, const Expr *E) {
   if (const CallExpr *CE = dyn_cast<CallExpr>(E)) {
     // FIXME: This doesn't handle indirect calls.
@@ -1966,11 +1998,12 @@ static void printExpectedAllocName(raw_ostream &os, AllocationFamily Family) {
   case AF_InnerBuffer:
     os << "container-specific allocator";
     return;
+  case AF_Custom:
+    os << Family.name().value();
+    return;
   case AF_Alloca:
   case AF_None:
     llvm_unreachable("not a deallocation expression");
-  case AF_Custom:
-    llvm_unreachable("not a deallocation expression");
   }
 }
 
@@ -1991,11 +2024,12 @@ static void printExpectedDeallocName(raw_ostream &os, AllocationFamily Family) {
   case AF_InnerBuffer:
     os << "container-specific deallocator";
     return;
+  case AF_Custom:
+    os << "function that takes ownership of '" << Family.name().value() << "\'";
+    return;
   case AF_Alloca:
   case AF_None:
     llvm_unreachable("suspicious argument");
-  case AF_Custom:
-    llvm_unreachable("suspicious argument");
   }
 }
 
@@ -2180,6 +2214,7 @@ MallocChecker::getCheckIfTracked(AllocationFamily Family,
   switch (Family.kind()) {
   case AF_Malloc:
   case AF_Alloca:
+  case AF_Custom:
   case AF_IfNameIndex: {
     if (ChecksEnabled[CK_MallocChecker])
       return CK_MallocChecker;
@@ -2202,7 +2237,6 @@ MallocChecker::getCheckIfTracked(AllocationFamily Family,
       return CK_InnerPointerChecker;
     return std::nullopt;
   }
-  case AF_Custom:
   case AF_None: {
     llvm_unreachable("no family");
   }
@@ -2432,6 +2466,8 @@ void MallocChecker::HandleMismatchedDealloc(CheckerContext &C,
 
         if (printMemFnName(DeallocOs, C, DeallocExpr))
           os << ", not " << DeallocOs.str();
+
+        printOwnershipTakesList(os, C, DeallocExpr);
     }
 
     auto R = std::make_unique<PathSensitiveBugReport>(*BT_MismatchedDealloc,
@@ -3545,6 +3581,7 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
       switch (Family.kind()) {
       case AF_Alloca:
       case AF_Malloc:
+      case AF_Custom:
       case AF_CXXNew:
       case AF_CXXNewArray:
       case AF_IfNameIndex:
@@ -3585,8 +3622,7 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N,
         }
         Msg = OS.str();
         break;
-        }
-        case AF_Custom:
+      }
         case AF_None:
           llvm_unreachable("Unhandled allocation family!");
         }

>From 0df5e138706e0db69f5c405b9712d3f26a7199c1 Mon Sep 17 00:00:00 2001
From: Pavel Skripkin <paskripkin at gmail.com>
Date: Mon, 15 Jul 2024 00:57:21 +0300
Subject: [PATCH 3/3] csa/tests: add test suite for custom allocation classes

---
 .../MismatchedDeallocator-checker-test.mm     | 42 +++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/clang/test/Analysis/MismatchedDeallocator-checker-test.mm b/clang/test/Analysis/MismatchedDeallocator-checker-test.mm
index 013d677e515cf..ef87951c0131e 100644
--- a/clang/test/Analysis/MismatchedDeallocator-checker-test.mm
+++ b/clang/test/Analysis/MismatchedDeallocator-checker-test.mm
@@ -14,6 +14,13 @@
 void free(void *);
 void __attribute((ownership_takes(malloc, 1))) my_free(void *);
 
+void __attribute((ownership_returns(malloc1))) *my_malloc1(size_t);
+void __attribute((ownership_takes(malloc1, 1))) my_free1(void *);
+
+void __attribute((ownership_returns(malloc2))) *my_malloc2(size_t);
+void __attribute((ownership_returns(malloc2))) *my_malloc3(size_t);
+void __attribute((ownership_takes(malloc2, 1))) __attribute((ownership_takes(malloc3, 1))) my_free23(void *);
+
 //---------------------------------------------------------------
 // Test if an allocation function matches deallocation function
 //---------------------------------------------------------------
@@ -60,6 +67,41 @@ void testMalloc8() {
   operator delete[](p); // expected-warning{{Memory allocated by malloc() should be deallocated by free(), not operator delete[]}}
 }
 
+void testMalloc9() {
+  int *p = (int *)my_malloc(sizeof(int));
+  my_free(p); // no warning
+}
+
+void testMalloc10() {
+  int *p = (int *)my_malloc1(sizeof(int));
+  my_free1(p); // no warning
+}
+
+void testMalloc11() {
+  int *p = (int *)my_malloc2(sizeof(int));
+  my_free23(p); // no warning
+}
+
+void testMalloc12() {
+  int *p = (int *)my_malloc1(sizeof(int));
+  my_free(p); // expected-warning{{Memory allocated by my_malloc1() should be deallocated by function that takes ownership of 'malloc1', not my_free(), which takes ownership of 'malloc'}}
+}
+
+void testMalloc13() {
+  int *p = (int *)my_malloc2(sizeof(int));
+  my_free1(p); // expected-warning{{Memory allocated by my_malloc2() should be deallocated by function that takes ownership of 'malloc2', not my_free1(), which takes ownership of 'malloc1'}}
+}
+
+void testMalloc14() {
+  int *p = (int *)my_malloc1(sizeof(int));
+  my_free23(p); // expected-warning{{Memory allocated by my_malloc1() should be deallocated by function that takes ownership of 'malloc1', not my_free23(), which takes ownership of 'malloc2', 'malloc3'}}
+}
+
+void testMalloc15() {
+  int *p = (int *)my_malloc1(sizeof(int));
+  free(p); // expected-warning{{Memory allocated by my_malloc1() should be deallocated by function that takes ownership of 'malloc1', not free()}}
+}
+
 void testAlloca() {
   int *p = (int *)__builtin_alloca(sizeof(int));
   delete p; // expected-warning{{Memory allocated by alloca() should not be deallocated}}



More information about the cfe-commits mailing list