Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7d1c700
[clang-analyzer] Add regression test for PR60896
ivanmurashko Aug 8, 2025
f6d1a05
[clang-analizer] MallocChecker: fix false positive leak for unique_pt…
ivanmurashko Aug 8, 2025
650b0f7
[clang-analyzer] MallocChecker: extend false positive leak fix to sup…
ivanmurashko Aug 8, 2025
0cc838f
[clang-analyzer] Apply clang-format
ivanmurashko Aug 8, 2025
333e5ba
[analyzer][test] Refactor smart pointer leak suppression and combine …
ivanmurashko Aug 9, 2025
07cfed9
[analyzer] MallocChecker: Address minor style and review comments
ivanmurashko Aug 9, 2025
66bf4e6
[analyzer] MallocChecker: Factor out smart pointer name check
ivanmurashko Aug 10, 2025
2dc6776
[clang-analyzer] Fix addTransition misuse - consolidate state updates
ivanmurashko Aug 10, 2025
83173d0
[analyzer][test] Add multiple owning arguments test case
ivanmurashko Aug 10, 2025
fddc1b4
[analyzer] Simplify MallocChecker::checkPostCall
ivanmurashko Aug 11, 2025
617a02a
[analyzer] scanReachableSymbols is expensive, so we use a single visi…
ivanmurashko Aug 11, 2025
c007afd
[analyzer] Fix overly broad escape logic in MallocChecker for mixed o…
ivanmurashko Aug 15, 2025
bb5eacc
[analyzer] Refactor MallocChecker::checkPostCall to improve readability
ivanmurashko Aug 15, 2025
db60663
[analyzer][test] Reorganize NewDeleteLeaks-PR60896 test into unified …
ivanmurashko Aug 15, 2025
7df1f75
[analyzer] Extract isSmartPtrName helper, add comments, and improve r…
ivanmurashko Aug 15, 2025
bb0d4f1
[analyzer] Fix addTransition API misuse in MallocChecker::checkPostCall
ivanmurashko Aug 15, 2025
a166bb3
[analyzer] Rename function and use set semantics in MallocChecker
ivanmurashko Aug 22, 2025
b4b9062
[analyzer] Fix out-of-bounds access in handleSmartPointerConstructorA…
ivanmurashko Aug 22, 2025
b8f4620
Update clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
ivanmurashko Aug 22, 2025
08a24ef
[analyzer] Refactor smart pointer detection and fix naming consistency
ivanmurashko Aug 28, 2025
f35ac73
Update clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
ivanmurashko Aug 30, 2025
d01533e
Update clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
ivanmurashko Aug 30, 2025
10569b6
Update clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
ivanmurashko Aug 30, 2025
f41c0bc
Update clang/test/Analysis/NewDeleteLeaks-PR60896.cpp
ivanmurashko Aug 30, 2025
c955216
Update clang/test/Analysis/NewDeleteLeaks-PR60896.cpp
ivanmurashko Aug 30, 2025
7c6ffa8
[analyzer] Eliminate duplicate traversal logic in MallocChecker smart…
ivanmurashko Aug 29, 2025
64d3aef
[analyzer] Make EscapeTrackedCallback::VisitSymbol public
ivanmurashko Aug 29, 2025
ee1c3ab
LIT tests updates to reflect the heuristic behaviour in the tests (sh…
ivanmurashko Aug 30, 2025
692db9e
[analyzer] Refactoring (name change): SmartOwningPtr -> SmartPtr
ivanmurashko Aug 30, 2025
49b7d31
[analyzer] Rename Base variables in MallocChecker to avoid confusion …
ivanmurashko Aug 30, 2025
d472fc4
[analyzer] Move variadic constructor test to separate namespace and u…
ivanmurashko Aug 30, 2025
6dd7671
Update clang/test/Analysis/NewDeleteLeaks-PR60896.cpp
ivanmurashko Sep 3, 2025
196abdb
[analyzer] Document dual behavior of hasSmartPtrField function
ivanmurashko Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 305 additions & 2 deletions clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the EscapeTrackedCallback class is properly wrapped in an anonymous namespace that starts at line 1094. It shares the same anonymous namespace with the StopTrackingCallback class defined just above it.

ProgramStateRef State;

explicit EscapeTrackedCallback(ProgramStateRef S) : State(std::move(S)) {}

public:
bool VisitSymbol(SymbolRef Sym) override {
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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 checkPreStmt but recognized this was architecturally suboptimal, suggesting it should be part of a "generic symbol escape callback" instead.

This TODO was rendered unnecessary when commit 122171e refactored the code by extracting the escape logic into a dedicated checkEscapeOnReturn method that could be reused by multiple callbacks (checkPreStmt and checkEndFunction). This addressed the architectural concern without requiring a complete redesign of the symbol escape callback system.

if (const MemRegion *MR = RetVal.getAsRegion())
if (isa<FieldRegion, ElementRegion>(MR))
if (const SymbolicRegion *BMR =
Expand Down
Loading