[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