[clang] [analyzer] Model GCC 'cleanup' attribute function calls (PR #221110)

via cfe-commits cfe-commits at lists.llvm.org
Thu Sep 10 01:01:51 PDT 2026


https://github.com/PengZheng updated https://github.com/llvm/llvm-project/pull/221110

>From b687f9e10ba5a2c7d587cc49c30ff7b03b9f5f8d Mon Sep 17 00:00:00 2001
From: PengZheng <howtofly at gmail.com>
Date: Thu, 3 Sep 2026 22:37:02 +0800
Subject: [PATCH 1/5] [analyzer] Model GCC 'cleanup' attribute function calls

Add CleanupFunctionCall, a Decl-origin CallEvent representing the implicit f(&var) call emitted when a __attribute__((cleanup(f))) variable goes out of scope, and process CFGCleanupFunction CFG elements in ExprEngine.

Update LiveVariables and PathDiagnostic to handle the new CFG element, and make CallEvent consumers resilient to Decl-origin arguments that have no source expression (parameter binding, NullabilityChecker, RetainCountChecker).
---
 .../Core/PathSensitive/CallEvent.h            |  69 +++++++-
 .../Core/PathSensitive/ExprEngine.h           |   3 +
 clang/lib/Analysis/LiveVariables.cpp          |   6 +
 clang/lib/Analysis/PathDiagnostic.cpp         |   8 +-
 .../Checkers/NullabilityChecker.cpp           |   8 +-
 .../RetainCountChecker/RetainCountChecker.cpp |   3 +-
 clang/lib/StaticAnalyzer/Core/CallEvent.cpp   |  33 +++-
 clang/lib/StaticAnalyzer/Core/ExprEngine.cpp  |  42 +++++
 clang/test/Analysis/gcc-cleanup-attr-160527.c |  32 ++++
 .../Analysis/gcc-cleanup-attr-diagnostics.c   |  37 +++++
 clang/test/Analysis/gcc-cleanup-attr.c        | 151 ++++++++++++++++++
 11 files changed, 382 insertions(+), 10 deletions(-)
 create mode 100644 clang/test/Analysis/gcc-cleanup-attr-160527.c
 create mode 100644 clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
 create mode 100644 clang/test/Analysis/gcc-cleanup-attr.c

diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
index 2010e4b0da84b..a0e4b0bc76829 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
@@ -70,8 +70,9 @@ enum CallEventKind {
   CE_END_CXX_CONSTRUCTOR_CALLS = CE_CXXInheritedConstructor,
   CE_CXXAllocator,
   CE_CXXDeallocator,
+  CE_CleanupFunction,
   CE_BEG_FUNCTION_CALLS = CE_Function,
-  CE_END_FUNCTION_CALLS = CE_CXXDeallocator,
+  CE_END_FUNCTION_CALLS = CE_CleanupFunction,
   CE_Block,
   CE_ObjCMessage
 };
@@ -430,6 +431,11 @@ class CallEvent {
   /// not do that because we don't know how (i.e., construction context is
   /// unavailable in the CFG or not supported by the analyzer).
   bool isArgumentConstructedDirectly(unsigned Index) const {
+    // Decl-origin calls (e.g. cleanup functions) have no argument expression,
+    // so nothing can have been constructed directly into an argument.
+    if (!getOriginExpr())
+      return false;
+
     // This assumes that the object was not yet removed from the state.
     return ExprEngine::getObjectUnderConstruction(
                getState(), {getOriginExpr(), Index}, getStackFrame())
@@ -1239,6 +1245,60 @@ class CXXDeallocatorCall : public AnyFunctionCall {
   }
 };
 
+/// Represents an implicit call to a cleanup function, triggered by a
+/// `__attribute__((cleanup(f)))` variable going out of scope.
+///
+/// The call has no syntactic representation: like \c CXXDestructorCall it is
+/// Decl-origin, and its single argument, the address of the annotated
+/// variable, is not written in the source.
+class CleanupFunctionCall : public AnyFunctionCall {
+  friend class CallEventManager;
+
+protected:
+  CleanupFunctionCall(const FunctionDecl *FD, const VarDecl *VD,
+                      ProgramStateRef St, const StackFrame *SF,
+                      CFGBlock::ConstCFGElementRef ElemRef)
+      : AnyFunctionCall(FD, St, SF, ElemRef) {
+    Data = VD;
+    Location = VD->getAttr<CleanupAttr>()->getLoc();
+  }
+
+  CleanupFunctionCall(const CleanupFunctionCall &Other) = default;
+
+  void cloneTo(void *Dest) const override {
+    new (Dest) CleanupFunctionCall(*this);
+  }
+
+public:
+  /// Returns the variable declaration whose scope exit triggered this call.
+  const VarDecl *getVarDecl() const {
+    return static_cast<const VarDecl *>(Data);
+  }
+
+  SourceRange getSourceRange() const override { return Location; }
+
+  unsigned getNumArgs() const override { return 1; }
+
+  // The implicit `&var` argument has no expression in the source.
+  const Expr *getArgExpr(unsigned Index) const override { return nullptr; }
+
+  SVal getArgSVal(unsigned Index) const override {
+    assert(Index == 0);
+    return getState()->getLValue(getVarDecl(), getStackFrame());
+  }
+
+  SourceRange getArgSourceRange(unsigned Index) const override {
+    return getSourceRange();
+  }
+
+  Kind getKind() const override { return CE_CleanupFunction; }
+  StringRef getKindAsString() const override { return "CleanupFunctionCall"; }
+
+  static bool classof(const CallEvent *CA) {
+    return CA->getKind() == CE_CleanupFunction;
+  }
+};
+
 /// Represents the ways an Objective-C message send can occur.
 //
 // Note to maintainers: OCM_Message should always be last, since it does not
@@ -1473,6 +1533,13 @@ class CallEventManager {
                         CFGBlock::ConstCFGElementRef ElemRef) {
     return create<CXXDeallocatorCall>(E, State, SF, ElemRef);
   }
+
+  CallEventRef<CleanupFunctionCall>
+  getCleanupFunctionCall(const FunctionDecl *FD, const VarDecl *VD,
+                         ProgramStateRef State, const StackFrame *SF,
+                         CFGBlock::ConstCFGElementRef ElemRef) {
+    return create<CleanupFunctionCall>(FD, VD, State, SF, ElemRef);
+  }
 };
 
 template <typename T>
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
index 195d63b0e0936..6c127d4678658 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h
@@ -338,6 +338,9 @@ class ExprEngine {
 
   void ProcessImplicitDtor(const CFGImplicitDtor D, ExplodedNode *Pred);
 
+  void ProcessCleanupFunction(const CFGCleanupFunction Cleanup,
+                              ExplodedNode *Pred);
+
   void ProcessNewAllocator(const CXXNewExpr *NE, ExplodedNode *Pred);
 
   void ProcessAutomaticObjDtor(const CFGAutomaticObjDtor D,
diff --git a/clang/lib/Analysis/LiveVariables.cpp b/clang/lib/Analysis/LiveVariables.cpp
index c9e547ac85380..f2ee59f21f2b5 100644
--- a/clang/lib/Analysis/LiveVariables.cpp
+++ b/clang/lib/Analysis/LiveVariables.cpp
@@ -488,6 +488,12 @@ LiveVariablesImpl::runOnBlock(const CFGBlock *block,
       continue;
     }
 
+    if (std::optional<CFGCleanupFunction> Cleanup =
+            elem.getAs<CFGCleanupFunction>()) {
+      val.liveDecls = DSetFact.add(val.liveDecls, Cleanup->getVarDecl());
+      continue;
+    }
+
     if (!elem.getAs<CFGStmt>())
       continue;
 
diff --git a/clang/lib/Analysis/PathDiagnostic.cpp b/clang/lib/Analysis/PathDiagnostic.cpp
index be4b6b5c5651c..db481b7e4b706 100644
--- a/clang/lib/Analysis/PathDiagnostic.cpp
+++ b/clang/lib/Analysis/PathDiagnostic.cpp
@@ -559,9 +559,15 @@ static PathDiagnosticLocation getLocationForCaller(const StackFrame *SF,
     return PathDiagnosticLocation::createEnd(Dtor.getBindTemporaryExpr(), SM,
                                              CallerSF);
   }
+  case CFGElement::CleanupFunction: {
+    const CFGCleanupFunction &Cleanup = Source.castAs<CFGCleanupFunction>();
+    // The implicit call is not written in the source; anchor it at the
+    // function name in the cleanup attribute.
+    const CleanupAttr *A = Cleanup.getVarDecl()->getAttr<CleanupAttr>();
+    return PathDiagnosticLocation(A->getLoc(), SM);
+  }
   case CFGElement::ScopeBegin:
   case CFGElement::ScopeEnd:
-  case CFGElement::CleanupFunction:
     llvm_unreachable("not yet implemented!");
   case CFGElement::LifetimeEnds:
   case CFGElement::FullExprCleanup:
diff --git a/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
index a01143bd57949..ee4692bb02578 100644
--- a/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
@@ -796,8 +796,12 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call,
 
     Nullability RequiredNullability =
         getNullabilityAnnotation(Param->getType());
-    Nullability ArgExprTypeLevelNullability =
-        getNullabilityAnnotation(lookThroughImplicitCasts(ArgExpr)->getType());
+    // Implicit calls (e.g. cleanup functions) may have arguments without a
+    // corresponding expression; there is no type-level nullability to read.
+    Nullability ArgExprTypeLevelNullability = Nullability::Unspecified;
+    if (ArgExpr)
+      ArgExprTypeLevelNullability =
+          getNullabilityAnnotation(lookThroughImplicitCasts(ArgExpr)->getType());
 
     unsigned ParamIdx = Param->getFunctionScopeIndex() + 1;
 
diff --git a/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
index 1ebf4787da67c..c71658b98ace0 100644
--- a/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
@@ -352,8 +352,7 @@ const static RetainSummary *getSummary(RetainSummaryManager &Summaries,
                                        QualType ReceiverType) {
   const Expr *CE = Call.getOriginExpr();
   AnyCall C =
-      CE ? *AnyCall::forExpr(CE)
-         : AnyCall(cast<CXXDestructorDecl>(Call.getDecl()));
+      CE ? *AnyCall::forExpr(CE) : *AnyCall::forDecl(Call.getDecl());
   return Summaries.getSummary(C, Call.hasNonZeroCallbackArg(),
                               isReceiverUnconsumedSelf(Call), ReceiverType);
 }
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 2338c06d5f992..6d2de766022ee 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -417,6 +417,12 @@ static bool isTransparentUnion(QualType T) {
 static SVal processArgument(SVal Value, const Expr *ArgumentExpr,
                             const ParmVarDecl *Parameter, SValBuilder &SVB) {
   QualType ParamType = Parameter->getType();
+
+  // Decl-origin calls (e.g. cleanup functions) have arguments without a
+  // corresponding expression. There is nothing to fix up for these.
+  if (!ArgumentExpr)
+    return Value;
+
   QualType ArgumentType = ArgumentExpr->getType();
 
   // Transparent unions allow users to easily convert values of union field
@@ -471,6 +477,9 @@ static SVal castArgToParamTypeIfNeeded(const CallEvent &Call, unsigned ArgIdx,
     return UnknownVal();
 
   const Expr *ArgExpr = Call.getArgExpr(ArgIdx);
+  if (!ArgExpr)
+    return ArgVal;
+
   const ParmVarDecl *Param = Definition->getParamDecl(ArgIdx);
   return SVB.evalCast(ArgVal, Param->getType(), ArgExpr->getType());
 }
@@ -508,8 +517,16 @@ static void addParameterValuesToBindings(const StackFrame *CalleeSF,
     // edge-cases.
     ArgVal = castArgToParamTypeIfNeeded(Call, Idx, ArgVal, SVB);
 
-    Loc ParamLoc = SVB.makeLoc(
-        MRMgr.getParamVarRegion(Call.getOriginExpr(), Idx, CalleeSF));
+    // The parameter region is keyed on the call expression when there is one;
+    // Decl-origin calls (e.g. cleanup functions) have no call site, so the
+    // callee body resolves parameters via MemRegionManager::getVarRegion.
+    // Bind to exactly that region.
+    const MemRegion *ParamRegion =
+        Call.getOriginExpr()
+            ? static_cast<const MemRegion *>(MRMgr.getParamVarRegion(
+                  Call.getOriginExpr(), Idx, CalleeSF))
+            : MRMgr.getVarRegion(*I, CalleeSF);
+    Loc ParamLoc = SVB.makeLoc(ParamRegion);
     Bindings.push_back(
         std::make_pair(ParamLoc, processArgument(ArgVal, ArgExpr, *I, SVB)));
   }
@@ -1477,10 +1494,18 @@ CallEventRef<> CallEventManager::getCaller(const StackFrame *CalleeSF,
     llvm_unreachable("This is not an inlineable statement");
   }
 
-  // Fall back to the CFG. The only thing we haven't handled yet is
-  // destructors, though this could change in the future.
+  // Fall back to the CFG. The only things we haven't handled yet are
+  // destructors and cleanup functions, though this could change in the future.
   const CFGBlock *B = CalleeSF->getCallSiteBlock();
   CFGElement E = (*B)[CalleeSF->getIndex()];
+
+  if (std::optional<CFGCleanupFunction> Cleanup =
+          E.getAs<CFGCleanupFunction>()) {
+    const auto *FD = cast<FunctionDecl>(CalleeSF->getDecl());
+    return getCleanupFunctionCall(FD, Cleanup->getVarDecl(), State, CallerSF,
+                                  ElemRef);
+  }
+
   assert((E.getAs<CFGImplicitDtor>() || E.getAs<CFGTemporaryDtor>()) &&
          "All other CFG elements should have exprs");
 
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
index f0f7d78fc5d50..69d1c0040e209 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
@@ -981,6 +981,8 @@ void ExprEngine::processCFGElement(const CFGElement E, ExplodedNode *Pred,
                          E.castAs<CFGLifetimeEnds>().getVarDecl(), Pred);
       return;
     case CFGElement::CleanupFunction:
+      ProcessCleanupFunction(E.castAs<CFGCleanupFunction>(), Pred);
+      return;
     case CFGElement::FullExprCleanup:
     case CFGElement::ScopeBegin:
     case CFGElement::ScopeEnd:
@@ -1393,6 +1395,46 @@ void ExprEngine::ProcessAutomaticObjDtor(const CFGAutomaticObjDtor Dtor,
                      /*IsBase=*/false, Pred, Dst, CallOpts);
 }
 
+void ExprEngine::ProcessCleanupFunction(const CFGCleanupFunction Cleanup,
+                                        ExplodedNode *Pred) {
+  const VarDecl *VD = Cleanup.getVarDecl();
+  const FunctionDecl *FD = Cleanup.getFunctionDecl();
+
+  ProgramStateRef State = Pred->getState();
+  const StackFrame *SF = Pred->getStackFrame();
+
+  // The implicit f(&var) call is not written in the source; anchor it at the
+  // function name in the cleanup attribute.
+  static SimpleProgramPointTag PT("ExprEngine",
+                                  "Prepare for cleanup function call");
+  PreImplicitCall PP(FD, VD->getAttr<CleanupAttr>()->getLoc(), SF,
+                     getCFGElementRef(), &PT);
+  Pred = Engine.makeNode(PP, State, Pred);
+
+  if (!Pred)
+    return;
+
+  CallEventManager &CEMgr = getStateManager().getCallEventManager();
+  CallEventRef<CleanupFunctionCall> Call = CEMgr.getCleanupFunctionCall(
+      FD, VD, Pred->getState(), SF, getCFGElementRef());
+
+  PrettyStackTraceLoc CrashInfo(getContext().getSourceManager(),
+                                Call->getSourceRange().getBegin(),
+                                "Error evaluating cleanup function");
+
+  ExplodedNodeSet Dst;
+  ExplodedNodeSet DstPreCall;
+  getCheckerManager().runCheckersForPreCall(DstPreCall, Pred, *Call, *this);
+
+  ExplodedNodeSet DstInvalidated;
+  for (ExplodedNode *N : DstPreCall)
+    defaultEvalCall(DstInvalidated, N, *Call);
+
+  getCheckerManager().runCheckersForPostCall(Dst, DstInvalidated, *Call, *this);
+
+  Engine.enqueueStmtNodes(Dst, getCurrBlock(), currStmtIdx);
+}
+
 void ExprEngine::ProcessDeleteDtor(const CFGDeleteDtor Dtor,
                                    ExplodedNode *Pred,
                                    ExplodedNodeSet &Dst) {
diff --git a/clang/test/Analysis/gcc-cleanup-attr-160527.c b/clang/test/Analysis/gcc-cleanup-attr-160527.c
new file mode 100644
index 0000000000000..355331df664f7
--- /dev/null
+++ b/clang/test/Analysis/gcc-cleanup-attr-160527.c
@@ -0,0 +1,32 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc,deadcode.DeadStores -verify %s
+// expected-no-diagnostics
+
+// Regression test for https://github.com/llvm/llvm-project/issues/160527:
+// a cleanup function that frees the pointee must not produce a false
+// "Potential leak" at an explicit return, and the assignment must not be
+// reported as a dead store.
+
+#include "Inputs/system-header-simulator-for-malloc.h"
+
+void *my_malloc(size_t size) {
+  return malloc(size);
+}
+
+// The cleanup function. It takes a pointer to the variable, so char** for a
+// char* variable (codegen calls free_pointer(&data)).
+static void free_pointer(char **p) {
+  free(*p);
+}
+
+void process_data(void) {
+  // The variable 'data' is tied to the 'free_pointer' function.
+  __attribute__((cleanup(free_pointer))) char *data = my_malloc(100);
+
+  if (!data) {
+    return;
+  }
+
+  // No explicit free(data) here: the cleanup function is called automatically
+  // when process_data() returns.
+  return;
+} // no leak on any path, and no dead-store warning on 'data'.
diff --git a/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c b/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
new file mode 100644
index 0000000000000..afda89962a144
--- /dev/null
+++ b/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
@@ -0,0 +1,37 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -analyzer-output=text -verify %s
+
+// Test that diagnostics cross the implicit cleanup call: the path describes
+// the call at the location of the cleanup attribute's function name, and
+// findings inside and around the inlined cleanup frame are reported.
+
+#include "Inputs/system-header-simulator-for-malloc.h"
+
+//===----------------------------------------------------------------------===//
+// A null dereference inside the cleanup body: the path enters the inlined
+// cleanup through a note anchored at the attribute.
+//===----------------------------------------------------------------------===//
+
+static void deref_cleanup(int **p) {
+  **p = 1; // expected-warning {{Dereference of null pointer}}
+           // expected-note at -1 {{Dereference of null pointer}}
+}
+
+void null_deref_in_cleanup(void) {
+  // The "Calling 'deref_cleanup'" note is anchored at the function name in
+  // the attribute.
+  int *p __attribute__((cleanup(deref_cleanup))); // expected-note {{Calling 'deref_cleanup'}}
+  p = 0; // expected-note {{Null pointer value stored to 'p'}}
+}
+
+//===----------------------------------------------------------------------===//
+// A leak through a no-op inlined cleanup frame: the report survives the
+// inlined cleanup call.
+//===----------------------------------------------------------------------===//
+
+static void empty_cleanup(int **p) { (void)p; }
+
+void leak_through_cleanup_frame(void) {
+  int *p __attribute__((cleanup(empty_cleanup)));
+  p = malloc(10); // expected-note {{Memory is allocated}}
+} // expected-warning {{Potential leak of memory pointed to by 'p'}}
+  // expected-note at -1 {{Potential leak of memory pointed to by 'p'}}
diff --git a/clang/test/Analysis/gcc-cleanup-attr.c b/clang/test/Analysis/gcc-cleanup-attr.c
new file mode 100644
index 0000000000000..5da3b56789c39
--- /dev/null
+++ b/clang/test/Analysis/gcc-cleanup-attr.c
@@ -0,0 +1,151 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc,deadcode.DeadStores,debug.ExprInspection -verify %s
+
+// Test modeling of GCC's __attribute__((cleanup(f))): the implicit f(&var)
+// call at scope exit is evaluated as an implicit call, inlined when a
+// definition is available and conservatively evaluated otherwise.
+
+#include "Inputs/system-header-simulator-for-malloc.h"
+
+void clang_analyzer_dump_int(int);
+void clang_analyzer_dump_ptr(void *);
+void clang_analyzer_numTimesReached(void);
+void clang_analyzer_warnIfReached(void);
+
+//===----------------------------------------------------------------------===//
+// The analysis continues past a scope exit with a cleanup-attributed variable.
+//===----------------------------------------------------------------------===//
+
+static void noop_cleanup(int *p) { (void)p; }
+
+void path_continues_after_scope(void) {
+  {
+    int x __attribute__((cleanup(noop_cleanup)));
+    x = 42; // no dead-store warning: the value is read by the cleanup call.
+  }
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+}
+
+//===----------------------------------------------------------------------===//
+// An inlined cleanup observes the address of the variable and the value last
+// stored to it.
+//===----------------------------------------------------------------------===//
+
+static void dump_cleanup(int *p) {
+  clang_analyzer_dump_ptr(p);  // expected-warning {{&x}}
+  clang_analyzer_dump_int(*p); // expected-warning {{42 S32b}}
+}
+
+void inlined_cleanup_observes_value(void) {
+  int x __attribute__((cleanup(dump_cleanup)));
+  x = 42;
+}
+
+//===----------------------------------------------------------------------===//
+// A declared-only cleanup is conservatively evaluated: the argument escapes
+// and no leak is reported for memory the unknown cleanup may have released.
+//===----------------------------------------------------------------------===//
+
+void declared_only_cleanup(void *p);
+
+void declared_only_cleanup_escapes(void) {
+  void *p __attribute__((cleanup(declared_only_cleanup)));
+  p = malloc(10);
+} // no leak: the pointer escapes into the conservatively evaluated call.
+
+//===----------------------------------------------------------------------===//
+// An inlined cleanup that frees the pointee: no leak.
+//===----------------------------------------------------------------------===//
+
+static void free_pointer_cleanup(char **p) {
+  free(*p);
+}
+
+void inlined_cleanup_frees(void) {
+  char *p __attribute__((cleanup(free_pointer_cleanup)));
+  p = malloc(10);
+} // no leak: free_pointer_cleanup(p) frees *p at the scope exit.
+
+//===----------------------------------------------------------------------===//
+// A non-releasing cleanup still leaks.
+//===----------------------------------------------------------------------===//
+
+static void non_releasing_cleanup(char **p) {
+  (void)p;
+}
+
+void non_releasing_cleanup_leaks(void) {
+  char *p __attribute__((cleanup(non_releasing_cleanup)));
+  p = malloc(10);
+} // expected-warning {{Potential leak of memory pointed to by 'p'}}
+
+//===----------------------------------------------------------------------===//
+// A double free through a cleanup function is anchored inside the cleanup
+// body.
+//===----------------------------------------------------------------------===//
+
+static void double_free_cleanup(char **p) {
+  free(*p);
+  free(*p); // expected-warning {{Attempt to release already released memory}}
+}
+
+void double_free_via_cleanup(void) {
+  char *p __attribute__((cleanup(double_free_cleanup)));
+  p = malloc(10);
+}
+
+//===----------------------------------------------------------------------===//
+// Directly naming a library function is conservatively evaluated: no crash
+// and no leak for the escaped memory.
+//===----------------------------------------------------------------------===//
+
+void direct_free_cleanup(void) {
+  // The emitted call is free(&p) and the compiler itself warns about it at
+  // the declaration; the analyzer evaluates the call conservatively and
+  // stays silent (no leak for the escaped pointee).
+  void *p __attribute__((cleanup(free))); // expected-warning {{attempt to call free on non-heap object 'p'}}
+  p = malloc(10);
+}
+
+//===----------------------------------------------------------------------===//
+// Struct, loop and early-return shapes.
+//===----------------------------------------------------------------------===//
+
+struct Wrapped {
+  char *p;
+};
+
+static void struct_cleanup(struct Wrapped *w) {
+  free(w->p);
+}
+
+void struct_shape(void) {
+  struct Wrapped w __attribute__((cleanup(struct_cleanup)));
+  w.p = malloc(10);
+} // no leak: struct_cleanup(w) frees w->p at the scope exit.
+
+static void loop_cleanup(int *p) {
+  clang_analyzer_numTimesReached(); // expected-warning {{4}}
+  (void)p;
+}
+
+int loop_shape(void) {
+  int sum = 0;
+  for (int i = 0; i < 10; ++i) {
+    int x __attribute__((cleanup(loop_cleanup)));
+    x = i;
+    sum += x;
+  }
+  return sum;
+}
+
+static void early_return_cleanup(char **p) {
+  free(*p);
+}
+
+int early_return_shape(void) {
+  char *p __attribute__((cleanup(early_return_cleanup)));
+  p = malloc(10);
+  if (!p)
+    return 1;
+  return 0;
+} // no leak on either path: the cleanup frees *p at the return.

>From e3ea5fc6bff641abd764bc90dd9e9a51de5b8dd1 Mon Sep 17 00:00:00 2001
From: PengZheng <howtofly at gmail.com>
Date: Tue, 8 Sep 2026 20:00:55 +0800
Subject: [PATCH 2/5] [analyzer] Fix missing switch-case and format issues
 reported by CI.

---
 clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp      | 4 ++--
 .../Checkers/RetainCountChecker/RetainCountChecker.cpp        | 3 +--
 clang/lib/StaticAnalyzer/Core/CallEvent.cpp                   | 4 ++--
 clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp     | 1 +
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
index ee4692bb02578..443dc7f38b8c8 100644
--- a/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/NullabilityChecker.cpp
@@ -800,8 +800,8 @@ void NullabilityChecker::checkPreCall(const CallEvent &Call,
     // corresponding expression; there is no type-level nullability to read.
     Nullability ArgExprTypeLevelNullability = Nullability::Unspecified;
     if (ArgExpr)
-      ArgExprTypeLevelNullability =
-          getNullabilityAnnotation(lookThroughImplicitCasts(ArgExpr)->getType());
+      ArgExprTypeLevelNullability = getNullabilityAnnotation(
+          lookThroughImplicitCasts(ArgExpr)->getType());
 
     unsigned ParamIdx = Param->getFunctionScopeIndex() + 1;
 
diff --git a/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
index c71658b98ace0..74436555303e8 100644
--- a/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp
@@ -351,8 +351,7 @@ const static RetainSummary *getSummary(RetainSummaryManager &Summaries,
                                        const CallEvent &Call,
                                        QualType ReceiverType) {
   const Expr *CE = Call.getOriginExpr();
-  AnyCall C =
-      CE ? *AnyCall::forExpr(CE) : *AnyCall::forDecl(Call.getDecl());
+  AnyCall C = CE ? *AnyCall::forExpr(CE) : *AnyCall::forDecl(Call.getDecl());
   return Summaries.getSummary(C, Call.hasNonZeroCallbackArg(),
                               isReceiverUnconsumedSelf(Call), ReceiverType);
 }
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 6d2de766022ee..3a7ffe2cf6964 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -523,8 +523,8 @@ static void addParameterValuesToBindings(const StackFrame *CalleeSF,
     // Bind to exactly that region.
     const MemRegion *ParamRegion =
         Call.getOriginExpr()
-            ? static_cast<const MemRegion *>(MRMgr.getParamVarRegion(
-                  Call.getOriginExpr(), Idx, CalleeSF))
+            ? static_cast<const MemRegion *>(
+                  MRMgr.getParamVarRegion(Call.getOriginExpr(), Idx, CalleeSF))
             : MRMgr.getVarRegion(*I, CalleeSF);
     Loc ParamLoc = SVB.makeLoc(ParamRegion);
     Bindings.push_back(
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
index 511bb036c5b68..76e2fa82e67c2 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngineCallAndReturn.cpp
@@ -841,6 +841,7 @@ ExprEngine::mayInlineCallKind(const CallEvent &Call, const ExplodedNode *Pred,
   case CE_Function:
   case CE_CXXStaticOperator:
   case CE_Block:
+  case CE_CleanupFunction:
     break;
   case CE_CXXMember:
   case CE_CXXMemberOperator:

>From a38d2459c87f762fcff076dd8ce240a017c5f30c Mon Sep 17 00:00:00 2001
From: PengZheng <howtofly at gmail.com>
Date: Tue, 8 Sep 2026 21:38:24 +0800
Subject: [PATCH 3/5] [NFC][analyzer] Address review feedback on cleanup
 function modeling

* Mention that getVarRegion() returns a NonParamVarRegion for parameters of Decl-origin calls without a call site.
* Use `auto` where the type is already spelled out in getAs<>().
* Make the getCaller() assertion message accurate.
* Explain why LiveVariables marks the cleanup variable live at the
  cleanup point.
---
 clang/lib/Analysis/LiveVariables.cpp        | 5 +++++
 clang/lib/StaticAnalyzer/Core/CallEvent.cpp | 9 +++++----
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/clang/lib/Analysis/LiveVariables.cpp b/clang/lib/Analysis/LiveVariables.cpp
index f2ee59f21f2b5..293e232f85e07 100644
--- a/clang/lib/Analysis/LiveVariables.cpp
+++ b/clang/lib/Analysis/LiveVariables.cpp
@@ -488,6 +488,11 @@ LiveVariablesImpl::runOnBlock(const CFGBlock *block,
       continue;
     }
 
+    // Like the destructor case above, the cleanup function call is an
+    // implicit use of the variable: it receives the variable's address, so it
+    // may still read the value stored in it. Marking the VarDecl live here
+    // prevents liveness-based analyses (e.g. deadcode.DeadStores) from
+    // treating the assignment that feeds the cleanup as a dead store.
     if (std::optional<CFGCleanupFunction> Cleanup =
             elem.getAs<CFGCleanupFunction>()) {
       val.liveDecls = DSetFact.add(val.liveDecls, Cleanup->getVarDecl());
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 3a7ffe2cf6964..0885b0afa2e32 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -519,7 +519,8 @@ static void addParameterValuesToBindings(const StackFrame *CalleeSF,
 
     // The parameter region is keyed on the call expression when there is one;
     // Decl-origin calls (e.g. cleanup functions) have no call site, so the
-    // callee body resolves parameters via MemRegionManager::getVarRegion.
+    // callee body resolves parameters via MemRegionManager::getVarRegion,
+    // which returns a NonParamVarRegion to represent the parameter.
     // Bind to exactly that region.
     const MemRegion *ParamRegion =
         Call.getOriginExpr()
@@ -1499,15 +1500,15 @@ CallEventRef<> CallEventManager::getCaller(const StackFrame *CalleeSF,
   const CFGBlock *B = CalleeSF->getCallSiteBlock();
   CFGElement E = (*B)[CalleeSF->getIndex()];
 
-  if (std::optional<CFGCleanupFunction> Cleanup =
-          E.getAs<CFGCleanupFunction>()) {
+  if (const auto Cleanup = E.getAs<CFGCleanupFunction>()) {
     const auto *FD = cast<FunctionDecl>(CalleeSF->getDecl());
     return getCleanupFunctionCall(FD, Cleanup->getVarDecl(), State, CallerSF,
                                   ElemRef);
   }
 
   assert((E.getAs<CFGImplicitDtor>() || E.getAs<CFGTemporaryDtor>()) &&
-         "All other CFG elements should have exprs");
+         "All other CFG elements have exprs or are cleanup functions, "
+         "represented by a CleanupFunctionCall");
 
   SValBuilder &SVB = State->getStateManager().getSValBuilder();
   const auto *Dtor = cast<CXXDestructorDecl>(CalleeSF->getDecl());

>From 559129df6f822fbb659924d173acac67ab9b04fb Mon Sep 17 00:00:00 2001
From: PengZheng <howtofly at gmail.com>
Date: Thu, 10 Sep 2026 12:19:15 +0800
Subject: [PATCH 4/5] [NFC][analyzer] Address review feedback on GCC cleanup
 attribute modeling

- Make the double-free test depend on the modeled cleanup call by moving
  the first free into the caller.
- Add scope-exit shape tests: gotos, reverse declaration order of
  cleanup handlers, cleanup variables nested in cleanup functions,
  interaction with C++ destructors, lambdas, and CSA inlining.
- Add a test that analyzes cleanup calls with a broad set of checkers,
  guarding the expressionless argument of CleanupFunctionCall against
  crashes.
- Pin the "Calling ..." diagnostic note to the location of the `cleanup`
  keyword and correct the misleading comments.
- Assert the single implicit argument index in CleanupFunctionCall::getArgExpr().
---
 .../Core/PathSensitive/CallEvent.h            |  9 +-
 clang/lib/Analysis/PathDiagnostic.cpp         |  4 +-
 clang/lib/StaticAnalyzer/Core/ExprEngine.cpp  |  4 +-
 .../test/Analysis/gcc-cleanup-attr-checkers.c | 27 ++++++
 clang/test/Analysis/gcc-cleanup-attr-cxx.cpp  | 68 +++++++++++++
 .../Analysis/gcc-cleanup-attr-diagnostics.c   | 24 ++++-
 clang/test/Analysis/gcc-cleanup-attr.c        | 97 ++++++++++++++++++-
 7 files changed, 221 insertions(+), 12 deletions(-)
 create mode 100644 clang/test/Analysis/gcc-cleanup-attr-checkers.c
 create mode 100644 clang/test/Analysis/gcc-cleanup-attr-cxx.cpp

diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
index a0e4b0bc76829..94570a1c0fd09 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
@@ -1250,7 +1250,9 @@ class CXXDeallocatorCall : public AnyFunctionCall {
 ///
 /// The call has no syntactic representation: like \c CXXDestructorCall it is
 /// Decl-origin, and its single argument, the address of the annotated
-/// variable, is not written in the source.
+/// variable, is not written in the source. The inherited \c getResultType()
+/// reports `void`: the return value of a cleanup function (if any) is always
+/// ignored.
 class CleanupFunctionCall : public AnyFunctionCall {
   friend class CallEventManager;
 
@@ -1280,7 +1282,10 @@ class CleanupFunctionCall : public AnyFunctionCall {
   unsigned getNumArgs() const override { return 1; }
 
   // The implicit `&var` argument has no expression in the source.
-  const Expr *getArgExpr(unsigned Index) const override { return nullptr; }
+  const Expr *getArgExpr(unsigned Index) const override {
+    assert(Index == 0);
+    return nullptr;
+  }
 
   SVal getArgSVal(unsigned Index) const override {
     assert(Index == 0);
diff --git a/clang/lib/Analysis/PathDiagnostic.cpp b/clang/lib/Analysis/PathDiagnostic.cpp
index db481b7e4b706..a3480c8662ded 100644
--- a/clang/lib/Analysis/PathDiagnostic.cpp
+++ b/clang/lib/Analysis/PathDiagnostic.cpp
@@ -562,7 +562,9 @@ static PathDiagnosticLocation getLocationForCaller(const StackFrame *SF,
   case CFGElement::CleanupFunction: {
     const CFGCleanupFunction &Cleanup = Source.castAs<CFGCleanupFunction>();
     // The implicit call is not written in the source; anchor it at the
-    // function name in the cleanup attribute.
+    // location of the `cleanup` attribute. (CleanupAttr::getLoc() returns
+    // the location of the `cleanup` keyword, not of the cleanup function
+    // name, which may sit on a different line.)
     const CleanupAttr *A = Cleanup.getVarDecl()->getAttr<CleanupAttr>();
     return PathDiagnosticLocation(A->getLoc(), SM);
   }
diff --git a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
index 69d1c0040e209..be71ef666e717 100644
--- a/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
+++ b/clang/lib/StaticAnalyzer/Core/ExprEngine.cpp
@@ -1404,7 +1404,7 @@ void ExprEngine::ProcessCleanupFunction(const CFGCleanupFunction Cleanup,
   const StackFrame *SF = Pred->getStackFrame();
 
   // The implicit f(&var) call is not written in the source; anchor it at the
-  // function name in the cleanup attribute.
+  // location of the `cleanup` attribute.
   static SimpleProgramPointTag PT("ExprEngine",
                                   "Prepare for cleanup function call");
   PreImplicitCall PP(FD, VD->getAttr<CleanupAttr>()->getLoc(), SF,
@@ -1422,7 +1422,6 @@ void ExprEngine::ProcessCleanupFunction(const CFGCleanupFunction Cleanup,
                                 Call->getSourceRange().getBegin(),
                                 "Error evaluating cleanup function");
 
-  ExplodedNodeSet Dst;
   ExplodedNodeSet DstPreCall;
   getCheckerManager().runCheckersForPreCall(DstPreCall, Pred, *Call, *this);
 
@@ -1430,6 +1429,7 @@ void ExprEngine::ProcessCleanupFunction(const CFGCleanupFunction Cleanup,
   for (ExplodedNode *N : DstPreCall)
     defaultEvalCall(DstInvalidated, N, *Call);
 
+  ExplodedNodeSet Dst;
   getCheckerManager().runCheckersForPostCall(Dst, DstInvalidated, *Call, *this);
 
   Engine.enqueueStmtNodes(Dst, getCurrBlock(), currStmtIdx);
diff --git a/clang/test/Analysis/gcc-cleanup-attr-checkers.c b/clang/test/Analysis/gcc-cleanup-attr-checkers.c
new file mode 100644
index 0000000000000..ec90b0332f387
--- /dev/null
+++ b/clang/test/Analysis/gcc-cleanup-attr-checkers.c
@@ -0,0 +1,27 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix,cplusplus,security,deadcode,nullability,optin.portability,optin.performance,optin.core,debug.ExprInspection -verify %s
+
+// Run the cleanup function modeling with a broad set of checkers: the
+// CleanupFunctionCall has an argument without a source expression, which
+// must not crash checkers that inspect call arguments.
+
+#include "Inputs/system-header-simulator-for-malloc.h"
+
+void clang_analyzer_warnIfReached(void);
+
+void declared_only_cleanup(void *p);
+
+static void noop_cleanup(int *p) {
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+  (void)p;
+}
+
+void many_checkers_with_cleanup(void) {
+  int x __attribute__((cleanup(noop_cleanup)));
+  x = 42; // no dead-store warning: the value is read by the cleanup call.
+  void *p __attribute__((cleanup(declared_only_cleanup)));
+  p = malloc(10);
+} // no leak: the memory escapes into the conservatively evaluated call.
+
+void analysis_continues_after_cleanup(void) {
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+}
diff --git a/clang/test/Analysis/gcc-cleanup-attr-cxx.cpp b/clang/test/Analysis/gcc-cleanup-attr-cxx.cpp
new file mode 100644
index 0000000000000..18bf437846c9c
--- /dev/null
+++ b/clang/test/Analysis/gcc-cleanup-attr-cxx.cpp
@@ -0,0 +1,68 @@
+// RUN: %clang_analyze_cc1 -x c++ -std=c++17 -analyzer-checker=core,unix.Malloc,deadcode.DeadStores,debug.ExprInspection -verify %s
+
+// Test scope-exit shapes of GCC's __attribute__((cleanup(f))) that are
+// specific to C++: interaction with destructors and lambdas.
+
+#include "Inputs/system-header-simulator-for-malloc.h"
+
+void clang_analyzer_dump_int(int);
+void clang_analyzer_warnIfReached(void);
+
+//===----------------------------------------------------------------------===//
+// For a variable with both a cleanup attribute and a non-trivial destructor,
+// the cleanup function runs before the destructor, matching the order in
+// which clang codegen emits the two calls.
+//===----------------------------------------------------------------------===//
+
+static int g;
+
+struct WithDtor {
+  ~WithDtor() { g = 2; }
+};
+
+static void cleanup_before_dtor_probe(struct WithDtor *p) {
+  clang_analyzer_dump_int(g); // expected-warning {{1 S32b}}
+  (void)p;
+}
+
+void cleanup_runs_before_destructor(void) {
+  struct WithDtor w __attribute__((cleanup(cleanup_before_dtor_probe)));
+  g = 1;
+} // The destructor would set g = 2; the dump above shows 1, so the cleanup
+  // function ran first.
+
+//===----------------------------------------------------------------------===//
+// A cleanup-annotated variable inside a lambda body: the cleanup runs when
+// the lambda call operator is inlined.
+//===----------------------------------------------------------------------===//
+
+static void lambda_cleanup(int *p) {
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+  (void)p;
+}
+
+void cleanup_in_lambda_body(void) {
+  auto lam = []() {
+    int z __attribute__((cleanup(lambda_cleanup)));
+    z = 1;
+  };
+  lam();
+}
+
+//===----------------------------------------------------------------------===//
+// A cleanup-annotated variable in a C++ function that the analyzer inlines:
+// the cleanup call is processed within the inlined stack frame.
+//===----------------------------------------------------------------------===//
+
+static void inlined_function_cleanup(int *p) {
+  clang_analyzer_dump_int(*p); // expected-warning {{42 S32b}}
+}
+
+static void inlined_function_with_cleanup(void) {
+  int x __attribute__((cleanup(inlined_function_cleanup)));
+  x = 42;
+}
+
+void cleanup_in_inlined_function(void) {
+  inlined_function_with_cleanup();
+}
diff --git a/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c b/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
index afda89962a144..b4779564380c9 100644
--- a/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
+++ b/clang/test/Analysis/gcc-cleanup-attr-diagnostics.c
@@ -1,8 +1,8 @@
 // RUN: %clang_analyze_cc1 -analyzer-checker=core,unix.Malloc -analyzer-output=text -verify %s
 
 // Test that diagnostics cross the implicit cleanup call: the path describes
-// the call at the location of the cleanup attribute's function name, and
-// findings inside and around the inlined cleanup frame are reported.
+// the call at the location of the `cleanup` attribute keyword, and findings
+// inside and around the inlined cleanup frame are reported.
 
 #include "Inputs/system-header-simulator-for-malloc.h"
 
@@ -17,8 +17,8 @@ static void deref_cleanup(int **p) {
 }
 
 void null_deref_in_cleanup(void) {
-  // The "Calling 'deref_cleanup'" note is anchored at the function name in
-  // the attribute.
+  // The "Calling 'deref_cleanup'" note is anchored at the `cleanup` keyword
+  // in the attribute.
   int *p __attribute__((cleanup(deref_cleanup))); // expected-note {{Calling 'deref_cleanup'}}
   p = 0; // expected-note {{Null pointer value stored to 'p'}}
 }
@@ -35,3 +35,19 @@ void leak_through_cleanup_frame(void) {
   p = malloc(10); // expected-note {{Memory is allocated}}
 } // expected-warning {{Potential leak of memory pointed to by 'p'}}
   // expected-note at -1 {{Potential leak of memory pointed to by 'p'}}
+
+//===----------------------------------------------------------------------===//
+// With the attribute split across lines, the "Calling ..." note is anchored
+// at the `cleanup` keyword, not at the function name on the next line.
+//===----------------------------------------------------------------------===//
+
+static void multiline_cleanup(int **p) {
+  **p = 1; // expected-warning {{Dereference of null pointer}}
+           // expected-note at -1 {{Dereference of null pointer}}
+}
+
+void null_deref_in_multiline_cleanup_attr(void) {
+  int *p __attribute__((cleanup( // expected-note {{Calling 'multiline_cleanup'}}
+      multiline_cleanup)));
+  p = 0; // expected-note {{Null pointer value stored to 'p'}}
+}
diff --git a/clang/test/Analysis/gcc-cleanup-attr.c b/clang/test/Analysis/gcc-cleanup-attr.c
index 5da3b56789c39..bb0f77e766bfa 100644
--- a/clang/test/Analysis/gcc-cleanup-attr.c
+++ b/clang/test/Analysis/gcc-cleanup-attr.c
@@ -79,18 +79,19 @@ void non_releasing_cleanup_leaks(void) {
 } // expected-warning {{Potential leak of memory pointed to by 'p'}}
 
 //===----------------------------------------------------------------------===//
-// A double free through a cleanup function is anchored inside the cleanup
-// body.
+// A double free through a cleanup function: the first free happens in the
+// caller, so the report depends on the modeled cleanup call at the scope
+// exit (without the cleanup attribute there would be no second free).
 //===----------------------------------------------------------------------===//
 
 static void double_free_cleanup(char **p) {
-  free(*p);
   free(*p); // expected-warning {{Attempt to release already released memory}}
 }
 
 void double_free_via_cleanup(void) {
   char *p __attribute__((cleanup(double_free_cleanup)));
   p = malloc(10);
+  free(p); // First free: the cleanup function releases the same pointer again.
 }
 
 //===----------------------------------------------------------------------===//
@@ -149,3 +150,93 @@ int early_return_shape(void) {
     return 1;
   return 0;
 } // no leak on either path: the cleanup frees *p at the return.
+
+//===----------------------------------------------------------------------===//
+// Scope-exit shapes: goto, cleanup ordering and nesting.
+//===----------------------------------------------------------------------===//
+
+// The cleanup runs on every exit from the scope, including jumps.
+
+static void goto_cleanup(int *p) {
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+  (void)p;
+}
+
+void goto_out_of_block_scope(void) {
+  {
+    int x __attribute__((cleanup(goto_cleanup)));
+    x = 1;
+    goto out;
+  }
+out:;
+}
+
+static void goto_cleanup_at_function_scope(int *p) {
+  clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+  (void)p;
+}
+
+void goto_at_function_scope(void) {
+  int x __attribute__((cleanup(goto_cleanup_at_function_scope)));
+  x = 1;
+  goto out;
+out:;
+}
+
+// Two cleanup handlers in the same scope run in reverse declaration order,
+// as in GCC.
+
+static int order_probe_global;
+
+static void order_probe(int *p) {
+  clang_analyzer_dump_int(order_probe_global); // expected-warning {{2 S32b}}
+  (void)p;
+}
+
+static void order_side_effect(int *p) {
+  order_probe_global = 2;
+  (void)p;
+}
+
+void cleanup_runs_in_reverse_declaration_order(void) {
+  int x __attribute__((cleanup(order_probe)));
+  int y __attribute__((cleanup(order_side_effect)));
+  x = 1;
+  y = 2;
+} // order_side_effect (declared last) runs first, so the dump above prints 2.
+
+// A cleanup handler can declare cleanup-attributed variables of its own:
+// the nested cleanup runs when the inlined handler exits.
+
+static void nested_cleanup(int *p) {
+  clang_analyzer_dump_int(*p); // expected-warning {{3 S32b}}
+}
+
+static void nested_handler(int *p) {
+  int z __attribute__((cleanup(nested_cleanup)));
+  z = 3;
+  (void)p;
+}
+
+void cleanup_nested_in_cleanup(void) {
+  int x __attribute__((cleanup(nested_handler)));
+  x = 42;
+}
+
+//===----------------------------------------------------------------------===//
+// A cleanup-annotated variable in a function that the analyzer inlines: the
+// cleanup call is processed within the inlined stack frame.
+//===----------------------------------------------------------------------===//
+
+static void inlined_function_cleanup(int *p) {
+  clang_analyzer_dump_int(*p); // expected-warning {{42 S32b}}
+}
+
+static void inlined_function_with_cleanup(void) {
+  int x __attribute__((cleanup(inlined_function_cleanup)));
+  x = 42;
+}
+
+void cleanup_in_inlined_function(void) {
+  inlined_function_with_cleanup();
+}

>From d8dcbdb3daff27da4bb73fb73efced57baf092d5 Mon Sep 17 00:00:00 2001
From: PengZheng <howtofly at gmail.com>
Date: Thu, 10 Sep 2026 16:01:33 +0800
Subject: [PATCH 5/5] [NFC][analyzer] Update getOriginExpr() documentation.

---
 .../clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h        | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
index 94570a1c0fd09..7e3efb07c32d6 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h
@@ -259,7 +259,8 @@ class CallEvent {
   virtual RuntimeDefinition getRuntimeDefinition() const = 0;
 
   /// Returns the expression whose value will be the result of this call.
-  /// Null if and only if 'this' is a CXXDestructorCall.
+  /// Null if and only if 'this' is a CXXDestructorCall or a
+  /// CleanupFunctionCall.
   virtual const Expr *getOriginExpr() const {
     return Origin.dyn_cast<const Expr *>();
   }



More information about the cfe-commits mailing list