-
Notifications
You must be signed in to change notification settings - Fork 14.9k
[analyzer] MallocChecker – Fix false positive leak for smart pointers in temporary objects #152751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7d1c700
f6d1a05
650b0f7
0cc838f
333e5ba
07cfed9
66bf4e6
2dc6776
83173d0
fddc1b4
617a02a
c007afd
bb5eacc
db60663
7df1f75
bb0d4f1
a166bb3
b4b9062
b8f4620
08a24ef
f35ac73
d01533e
10569b6
f41c0bc
c955216
7c6ffa8
64d3aef
ee1c3ab
692db9e
49b7d31
d472fc4
6dd7671
196abdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ | |
| #include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h" | ||
| #include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h" | ||
| #include "llvm/ADT/STLExtras.h" | ||
| #include "llvm/ADT/SmallVector.h" | ||
| #include "llvm/ADT/StringExtras.h" | ||
| #include "llvm/Support/Casting.h" | ||
| #include "llvm/Support/Compiler.h" | ||
|
|
@@ -421,6 +422,13 @@ class MallocChecker | |
| void checkPreCall(const CallEvent &Call, CheckerContext &C) const; | ||
| void checkPostCall(const CallEvent &Call, CheckerContext &C) const; | ||
| bool evalCall(const CallEvent &Call, CheckerContext &C) const; | ||
|
|
||
| ProgramStateRef | ||
| handleSmartPointerConstructorArguments(const CallEvent &Call, | ||
| ProgramStateRef State) const; | ||
| ProgramStateRef handleSmartPointerRelatedCalls(const CallEvent &Call, | ||
| CheckerContext &C, | ||
| ProgramStateRef State) const; | ||
| void checkNewAllocator(const CXXAllocatorCall &Call, CheckerContext &C) const; | ||
| void checkPostObjCMessage(const ObjCMethodCall &Call, CheckerContext &C) const; | ||
| void checkPostStmt(const BlockExpr *BE, CheckerContext &C) const; | ||
|
|
@@ -1096,6 +1104,54 @@ class StopTrackingCallback final : public SymbolVisitor { | |
| return true; | ||
| } | ||
| }; | ||
|
|
||
| /// EscapeTrackedCallback - A SymbolVisitor that marks allocated symbols as | ||
| /// escaped. | ||
| /// | ||
| /// This visitor is used to suppress false positive leak reports when smart | ||
| /// pointers are nested in temporary objects passed by value to functions. When | ||
| /// the analyzer can't see the destructor calls for temporary objects, it may | ||
| /// incorrectly report leaks for memory that will be properly freed by the smart | ||
| /// pointer destructors. | ||
| /// | ||
| /// The visitor traverses reachable symbols from a given set of memory regions | ||
| /// (typically smart pointer field regions) and marks any allocated symbols as | ||
| /// escaped. Escaped symbols are not reported as leaks by checkDeadSymbols. | ||
| class EscapeTrackedCallback final : public SymbolVisitor { | ||
| ProgramStateRef State; | ||
|
|
||
| explicit EscapeTrackedCallback(ProgramStateRef S) : State(std::move(S)) {} | ||
|
|
||
| public: | ||
| bool VisitSymbol(SymbolRef Sym) override { | ||
NagyDonat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (const RefState *RS = State->get<RegionState>(Sym)) { | ||
| if (RS->isAllocated() || RS->isAllocatedOfSizeZero()) { | ||
| State = State->set<RegionState>(Sym, RefState::getEscaped(RS)); | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /// Escape tracked regions reachable from the given roots. | ||
| static ProgramStateRef | ||
| EscapeTrackedRegionsReachableFrom(ArrayRef<const MemRegion *> Roots, | ||
| ProgramStateRef State) { | ||
| if (Roots.empty()) | ||
| return State; | ||
|
|
||
| // scanReachableSymbols is expensive, so we use a single visitor for all | ||
| // roots | ||
| SmallVector<const MemRegion *, 10> Regions; | ||
| EscapeTrackedCallback Visitor(State); | ||
| for (const MemRegion *R : Roots) { | ||
| Regions.push_back(R); | ||
| } | ||
| State->scanReachableSymbols(Regions, Visitor); | ||
| return Visitor.State; | ||
| } | ||
|
|
||
| friend class SymbolVisitor; | ||
| }; | ||
| } // end anonymous namespace | ||
|
|
||
| static bool isStandardNew(const FunctionDecl *FD) { | ||
|
|
@@ -3068,12 +3124,260 @@ void MallocChecker::checkDeadSymbols(SymbolReaper &SymReaper, | |
| C.addTransition(state->set<RegionState>(RS), N); | ||
| } | ||
|
|
||
| // Allowlist of owning smart pointers we want to recognize. | ||
| // Start with unique_ptr and shared_ptr; weak_ptr is excluded intentionally | ||
| // because it does not own the pointee. | ||
| static bool isSmartPtrName(StringRef Name) { | ||
| return Name == "unique_ptr" || Name == "shared_ptr"; | ||
| } | ||
|
|
||
| // Check if a type is a smart owning pointer type. | ||
| static bool isSmartPtrType(QualType QT) { | ||
| QT = QT->getCanonicalTypeUnqualified(); | ||
|
|
||
| if (const auto *TST = QT->getAs<TemplateSpecializationType>()) { | ||
| const TemplateDecl *TD = TST->getTemplateName().getAsTemplateDecl(); | ||
| if (!TD) | ||
| return false; | ||
|
|
||
| const auto *ND = dyn_cast_or_null<NamedDecl>(TD->getTemplatedDecl()); | ||
| if (!ND) | ||
| return false; | ||
|
|
||
| // For broader coverage we recognize all template classes with names that | ||
| // match the allowlist even if they are not declared in namespace 'std'. | ||
| return isSmartPtrName(ND->getName()); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// Helper struct for collecting smart owning pointer field regions. | ||
| /// This allows both hasSmartPtrField and | ||
| /// collectSmartPtrFieldRegions to share the same traversal logic, | ||
| /// ensuring consistency. | ||
| struct FieldConsumer { | ||
| const MemRegion *Reg; | ||
| CheckerContext *C; | ||
| llvm::SmallPtrSetImpl<const MemRegion *> *Out; | ||
|
|
||
| FieldConsumer(const MemRegion *Reg, CheckerContext &C, | ||
| llvm::SmallPtrSetImpl<const MemRegion *> &Out) | ||
| : Reg(Reg), C(&C), Out(&Out) {} | ||
|
|
||
| void consume(const FieldDecl *FD) { | ||
| SVal L = C->getState()->getLValue(FD, loc::MemRegionVal(Reg)); | ||
| if (const MemRegion *FR = L.getAsRegion()) | ||
| Out->insert(FR); | ||
| } | ||
|
|
||
| std::optional<FieldConsumer> switchToBase(const CXXRecordDecl *BaseDecl, | ||
| bool IsVirtual) { | ||
| // Get the base class region | ||
| SVal BaseL = | ||
| C->getState()->getLValue(BaseDecl, Reg->getAs<SubRegion>(), IsVirtual); | ||
| if (const MemRegion *BaseObjRegion = BaseL.getAsRegion()) { | ||
| // Return a consumer for the base class | ||
| return FieldConsumer{BaseObjRegion, *C, *Out}; | ||
| } | ||
| return std::nullopt; | ||
| } | ||
| }; | ||
|
|
||
| /// Check if a record type has smart owning pointer fields (directly or in base | ||
| /// classes). When FC is provided, also collect the field regions. | ||
| /// | ||
| /// This function has dual behavior: | ||
| /// - When FC is nullopt: Returns true if smart pointer fields are found | ||
| /// - When FC is provided: Always returns false, but collects field regions | ||
| /// as a side effect through the FieldConsumer | ||
| /// | ||
| /// Note: When FC is provided, the return value should be ignored since the | ||
| /// function performs full traversal for collection and always returns false | ||
| /// to avoid early termination. | ||
| static bool hasSmartPtrField(const CXXRecordDecl *CRD, | ||
| std::optional<FieldConsumer> FC = std::nullopt) { | ||
| // Check direct fields | ||
| for (const FieldDecl *FD : CRD->fields()) { | ||
| if (isSmartPtrType(FD->getType())) { | ||
| if (!FC) | ||
| return true; | ||
| FC->consume(FD); | ||
| } | ||
| } | ||
|
|
||
| // Check fields from base classes | ||
| for (const CXXBaseSpecifier &BaseSpec : CRD->bases()) { | ||
| if (const CXXRecordDecl *BaseDecl = | ||
| BaseSpec.getType()->getAsCXXRecordDecl()) { | ||
| std::optional<FieldConsumer> NewFC; | ||
| if (FC) { | ||
| NewFC = FC->switchToBase(BaseDecl, BaseSpec.isVirtual()); | ||
| if (!NewFC) | ||
| continue; | ||
| } | ||
| bool Found = hasSmartPtrField(BaseDecl, NewFC); | ||
| if (Found && !FC) | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /// Check if an expression is an rvalue record type passed by value. | ||
| static bool isRvalueByValueRecord(const Expr *AE) { | ||
| if (AE->isGLValue()) | ||
| return false; | ||
|
|
||
| QualType T = AE->getType(); | ||
| if (!T->isRecordType() || T->isReferenceType()) | ||
| return false; | ||
|
|
||
| // Accept common temp/construct forms but don't overfit. | ||
| return isa<CXXTemporaryObjectExpr, MaterializeTemporaryExpr, CXXConstructExpr, | ||
| InitListExpr, ImplicitCastExpr, CXXBindTemporaryExpr>(AE); | ||
| } | ||
|
|
||
| /// Check if an expression is an rvalue record with smart owning pointer fields | ||
| /// passed by value. | ||
| static bool isRvalueByValueRecordWithSmartPtr(const Expr *AE) { | ||
| if (!isRvalueByValueRecord(AE)) | ||
| return false; | ||
|
|
||
| const auto *CRD = AE->getType()->getAsCXXRecordDecl(); | ||
| return CRD && hasSmartPtrField(CRD); | ||
| } | ||
|
|
||
| /// Check if a CXXRecordDecl has a name matching recognized smart pointer names. | ||
| static bool isSmartPtrRecord(const CXXRecordDecl *RD) { | ||
| if (!RD) | ||
| return false; | ||
|
|
||
| // Check the record name directly and accept both std and custom smart pointer | ||
| // implementations for broader coverage | ||
| return isSmartPtrName(RD->getName()); | ||
| } | ||
|
|
||
| /// Check if a call is a constructor of a smart owning pointer class that | ||
| /// accepts pointer parameters. | ||
| static bool isSmartPtrCall(const CallEvent &Call) { | ||
| // Only check for smart pointer constructor calls | ||
| const auto *CD = dyn_cast_or_null<CXXConstructorDecl>(Call.getDecl()); | ||
| if (!CD) | ||
| return false; | ||
|
|
||
| const auto *RD = CD->getParent(); | ||
| if (!isSmartPtrRecord(RD)) | ||
| return false; | ||
|
|
||
| // Check if constructor takes a pointer parameter | ||
| for (const auto *Param : CD->parameters()) { | ||
| QualType ParamType = Param->getType(); | ||
| if (ParamType->isPointerType() && !ParamType->isFunctionPointerType() && | ||
| !ParamType->isVoidPointerType()) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// Collect memory regions of smart owning pointer fields from a record type | ||
| /// (including fields from base classes). | ||
| static void | ||
| collectSmartPtrFieldRegions(const MemRegion *Reg, QualType RecQT, | ||
| CheckerContext &C, | ||
| llvm::SmallPtrSetImpl<const MemRegion *> &Out) { | ||
| if (!Reg) | ||
| return; | ||
|
|
||
| const auto *CRD = RecQT->getAsCXXRecordDecl(); | ||
| if (!CRD) | ||
| return; | ||
|
|
||
| FieldConsumer FC{Reg, C, Out}; | ||
| hasSmartPtrField(CRD, FC); | ||
| } | ||
|
|
||
| /// Handle smart pointer constructor calls by escaping allocated symbols | ||
| /// that are passed as pointer arguments to the constructor. | ||
| ProgramStateRef MallocChecker::handleSmartPointerConstructorArguments( | ||
| const CallEvent &Call, ProgramStateRef State) const { | ||
| const auto *CD = cast<CXXConstructorDecl>(Call.getDecl()); | ||
| for (unsigned I = 0, E = std::min(Call.getNumArgs(), CD->getNumParams()); | ||
| I != E; ++I) { | ||
| const Expr *ArgExpr = Call.getArgExpr(I); | ||
| if (!ArgExpr) | ||
| continue; | ||
|
|
||
| QualType ParamType = CD->getParamDecl(I)->getType(); | ||
| if (ParamType->isPointerType() && !ParamType->isFunctionPointerType() && | ||
| !ParamType->isVoidPointerType()) { | ||
| // This argument is a pointer being passed to smart pointer constructor | ||
| SVal ArgVal = Call.getArgSVal(I); | ||
| SymbolRef Sym = ArgVal.getAsSymbol(); | ||
| if (Sym && State->contains<RegionState>(Sym)) { | ||
| const RefState *RS = State->get<RegionState>(Sym); | ||
| if (RS && (RS->isAllocated() || RS->isAllocatedOfSizeZero())) { | ||
| State = State->set<RegionState>(Sym, RefState::getEscaped(RS)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return State; | ||
| } | ||
|
|
||
| /// Handle all smart pointer related processing in function calls. | ||
| /// This includes both direct smart pointer constructor calls and by-value | ||
| /// arguments containing smart pointer fields. | ||
| ProgramStateRef MallocChecker::handleSmartPointerRelatedCalls( | ||
| const CallEvent &Call, CheckerContext &C, ProgramStateRef State) const { | ||
|
|
||
| // Handle direct smart pointer constructor calls first | ||
| if (isSmartPtrCall(Call)) { | ||
| return handleSmartPointerConstructorArguments(Call, State); | ||
| } | ||
|
|
||
| // Handle smart pointer fields in by-value record arguments | ||
| llvm::SmallPtrSet<const MemRegion *, 8> SmartPtrFieldRoots; | ||
| for (unsigned I = 0, E = Call.getNumArgs(); I != E; ++I) { | ||
| const Expr *AE = Call.getArgExpr(I); | ||
| if (!AE) | ||
| continue; | ||
| AE = AE->IgnoreParenImpCasts(); | ||
|
|
||
| if (!isRvalueByValueRecordWithSmartPtr(AE)) | ||
| continue; | ||
|
|
||
| // Find a region for the argument. | ||
| SVal ArgVal = Call.getArgSVal(I); | ||
| const MemRegion *ArgRegion = ArgVal.getAsRegion(); | ||
| // Collect direct smart owning pointer field regions | ||
| collectSmartPtrFieldRegions(ArgRegion, AE->getType(), C, | ||
| SmartPtrFieldRoots); | ||
| } | ||
|
|
||
| // Escape symbols reachable from smart pointer fields | ||
| if (!SmartPtrFieldRoots.empty()) { | ||
| SmallVector<const MemRegion *, 8> SmartPtrFieldRootsVec( | ||
| SmartPtrFieldRoots.begin(), SmartPtrFieldRoots.end()); | ||
| State = EscapeTrackedCallback::EscapeTrackedRegionsReachableFrom( | ||
| SmartPtrFieldRootsVec, State); | ||
| } | ||
|
|
||
| return State; | ||
| } | ||
|
|
||
| void MallocChecker::checkPostCall(const CallEvent &Call, | ||
| CheckerContext &C) const { | ||
| // Handle existing post-call handlers first | ||
| if (const auto *PostFN = PostFnMap.lookup(Call)) { | ||
| (*PostFN)(this, C.getState(), Call, C); | ||
| return; | ||
| return; // Post-handler already called addTransition, we're done | ||
| } | ||
|
|
||
| // Handle smart pointer related processing only if no post-handler was called | ||
| C.addTransition(handleSmartPointerRelatedCalls(Call, C, C.getState())); | ||
| } | ||
|
|
||
| void MallocChecker::checkPreCall(const CallEvent &Call, | ||
|
|
@@ -3194,7 +3498,6 @@ void MallocChecker::checkEscapeOnReturn(const ReturnStmt *S, | |
| if (!Sym) | ||
| // If we are returning a field of the allocated struct or an array element, | ||
| // the callee could still free the memory. | ||
| // TODO: This logic should be a part of generic symbol escape callback. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this just a random obsolete TODO, or is it fixed by the this PR? (No action excepted, just curious.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The change is not directly related to the PR. I believe that the TODO comment became obsolete a long time ago, and I deleted it for that reason. The TODO comment was added in February 2012 (see 4ca45b1) when fixing a false positive in the malloc checker related to returning fields of allocated structs or array elements via pointer arithmetic. The author of the commit embedded escape-on-return logic directly in This TODO was rendered unnecessary when commit 122171e refactored the code by extracting the escape logic into a dedicated |
||
| if (const MemRegion *MR = RetVal.getAsRegion()) | ||
| if (isa<FieldRegion, ElementRegion>(MR)) | ||
| if (const SymbolicRegion *BMR = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this class declaration wrapped in an anonymous namespace?
https://llvm.org/docs/CodingStandards.html#restrict-visibility
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the
EscapeTrackedCallbackclass is properly wrapped in an anonymous namespace that starts at line 1094. It shares the same anonymous namespace with theStopTrackingCallbackclass defined just above it.