https://github.com/PengZheng updated https://github.com/llvm/llvm-project/pull/221110
>From b687f9e10ba5a2c7d587cc49c30ff7b03b9f5f8d Mon Sep 17 00:00:00 2001 From: PengZheng <[email protected]> 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@-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@-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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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@-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@-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 <[email protected]> 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 *>(); } _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
