Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
247 changes: 246 additions & 1 deletion clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/TemplateBase.h"
#include "clang/AST/Type.h"

#include "clang/AST/ParentMap.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
Expand All @@ -78,6 +81,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 @@ -1096,6 +1100,41 @@ 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.
///
/// Usage:
/// auto Scan =
/// State->scanReachableSymbols<EscapeTrackedCallback>(RootRegions);
/// ProgramStateRef NewState = Scan.getState();
/// if (NewState != State) C.addTransition(NewState);
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;

public:
explicit EscapeTrackedCallback(ProgramStateRef S) : State(std::move(S)) {}
ProgramStateRef getState() const { return State; }

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;
}
};
} // end anonymous namespace

static bool isStandardNew(const FunctionDecl *FD) {
Expand Down Expand Up @@ -3068,11 +3107,217 @@ void MallocChecker::checkDeadSymbols(SymbolReaper &SymReaper,
C.addTransition(state->set<RegionState>(RS), N);
}

static QualType canonicalStrip(QualType QT) {
return QT.getCanonicalType().getUnqualifiedType();
}

static bool isInStdNamespace(const DeclContext *DC) {
while (DC) {
if (const auto *NS = dyn_cast<NamespaceDecl>(DC))
if (NS->isStdNamespace())
return true;
DC = DC->getParent();
}
return false;
}

// Allowlist of owning smart pointers we want to recognize.
// Start with unique_ptr and shared_ptr. (intentionally exclude weak_ptr)
static bool isSmartOwningPtrType(QualType QT) {
QT = canonicalStrip(QT);

// First try TemplateSpecializationType (for std smart pointers)
const auto *TST = QT->getAs<TemplateSpecializationType>();
if (TST) {
const TemplateDecl *TD = TST->getTemplateName().getAsTemplateDecl();
if (!TD)
return false;

const auto *ND = dyn_cast_or_null<NamedDecl>(TD->getTemplatedDecl());
if (!ND)
return false;

// Check if it's in std namespace
const DeclContext *DC = ND->getDeclContext();
if (!isInStdNamespace(DC))
return false;

StringRef Name = ND->getName();
return Name == "unique_ptr" || Name == "shared_ptr";
}

// Also try RecordType (for custom smart pointer implementations)
const auto *RT = QT->getAs<RecordType>();
if (RT) {
const auto *RD = RT->getDecl();
if (RD) {
StringRef Name = RD->getName();
if (Name == "unique_ptr" || Name == "shared_ptr") {
// Accept any custom unique_ptr or shared_ptr implementation
return true;
}
}
}

return false;
}

static void collectDirectSmartOwningPtrFieldRegions(
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this function no longer only collects direct field regions, as it also traverses the base subobjects.
Should we rename this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! You're absolutely right. I've renamed the function from collectDirectSmartOwningPtrFieldRegions to collectSmartOwningPtrFieldRegions to better reflect that it now traverses the entire inheritance hierarchy, not just direct fields (commit a166bb3).

const MemRegion *Base, QualType RecQT, CheckerContext &C,
Copy link
Contributor

Choose a reason for hiding this comment

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

The variable name const MemRegion *Base is a bit unfortunate because it appears to refer to the frequently used method MemRegion::getBaseRegion which uses the word "base" in a different meaning:

  • SomeRegion.getBaseRegion() returns the largest cohesive block of memory that contains the given region: e.g. calling it on the field region array[5].field.otherfield would return the region corresponding to array.
  • This variable name is called "base" because (at least in the recursive calls) it is the subobject corresponding to a certain base class.

(Perhaps this would be more clear for others, but during the first review I completely missed that this variable is not connected to getBaseRegion.)

To clarify the naming, I'd suggest:

  • renaming this parameter cost MemRegion *Base to e.g. const MemRegion *Reg (a natural name for "the" region);
  • later in the body of the function renaming BaseRegion to e.g. BaseObjRegion (to highlight that it's a region corresponding to a base object).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I applied the name change in 49b7d31.

SmallVectorImpl<const MemRegion *> &Out) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldnt out have set semantics?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! I've changed the parameter to use llvm::SmallPtrSetImpl<const MemRegion *> to ensure each region is only collected once, preventing duplicates from inheritance traversal (commit a166bb3).

if (!Base)
return;
const auto *CRD = RecQT->getAsCXXRecordDecl();
if (!CRD)
return;

for (const FieldDecl *FD : CRD->fields()) {
if (!isSmartOwningPtrType(FD->getType()))
continue;
SVal L = C.getState()->getLValue(FD, loc::MemRegionVal(Base));
if (const MemRegion *FR = L.getAsRegion())
Out.push_back(FR);
}
}

void MallocChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
// Keep existing post-call handlers.
if (const auto *PostFN = PostFnMap.lookup(Call)) {
(*PostFN)(this, C.getState(), Call, C);
return;
}

SmallVector<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();

QualType T = AE->getType();

// **Relaxation 1**: accept *any rvalue* by-value record (not only strict
// PRVALUE).
if (AE->isGLValue())
continue;

// By-value record only (no refs).
if (!T->isRecordType() || T->isReferenceType())
continue;

// **Relaxation 2**: accept common temp/construct forms but don't overfit.
const bool LooksLikeTemp =
isa<CXXTemporaryObjectExpr>(AE) || isa<MaterializeTemporaryExpr>(AE) ||
isa<CXXConstructExpr>(AE) || isa<InitListExpr>(AE) ||
isa<ImplicitCastExpr>(AE) || // handle common rvalue materializations
isa<CXXBindTemporaryExpr>(AE); // handle CXXBindTemporaryExpr
if (!LooksLikeTemp)
continue;

// Require at least one direct smart owning pointer field by type.
const auto *CRD = T->getAsCXXRecordDecl();
if (!CRD)
continue;
bool HasSmartPtrField = false;
for (const FieldDecl *FD : CRD->fields()) {
if (isSmartOwningPtrType(FD->getType())) {
HasSmartPtrField = true;
break;
}
}
if (!HasSmartPtrField)
continue;

// Find a region for the argument.
SVal VCall = Call.getArgSVal(I);
SVal VExpr = C.getSVal(AE);
const MemRegion *RCall = VCall.getAsRegion();
const MemRegion *RExpr = VExpr.getAsRegion();

const MemRegion *Base = RCall ? RCall : RExpr;
if (!Base) {
// Fallback: if we have a by-value record with unique_ptr fields but no
// region, mark all allocated symbols as escaped
ProgramStateRef State = C.getState();
RegionStateTy RS = State->get<RegionState>();
ProgramStateRef NewState = State;
for (auto [Sym, RefSt] : RS) {
if (RefSt.isAllocated() || RefSt.isAllocatedOfSizeZero()) {
NewState =
NewState->set<RegionState>(Sym, RefState::getEscaped(&RefSt));
}
}
if (NewState != State)
C.addTransition(NewState);
continue;
}

// Push direct smart owning pointer field regions only (precise root set).
collectDirectSmartOwningPtrFieldRegions(Base, T, C, SmartPtrFieldRoots);
}

// Escape only from those field roots; do nothing if empty.
if (!SmartPtrFieldRoots.empty()) {
ProgramStateRef State = C.getState();
auto Scan =
State->scanReachableSymbols<EscapeTrackedCallback>(SmartPtrFieldRoots);
ProgramStateRef NewState = Scan.getState();
if (NewState != State) {
C.addTransition(NewState);
} else {
// Fallback: if we have by-value record arguments but no smart pointer
// fields detected, check if any of the arguments are by-value records
// with smart pointer fields
bool hasByValueRecordWithSmartPtr = false;
for (unsigned I = 0, E = Call.getNumArgs(); I != E; ++I) {
const Expr *AE = Call.getArgExpr(I);
if (!AE)
continue;
AE = AE->IgnoreParenImpCasts();

if (AE->isGLValue())
continue;
QualType T = AE->getType();
if (!T->isRecordType() || T->isReferenceType())
continue;

const bool LooksLikeTemp =
isa<CXXTemporaryObjectExpr>(AE) ||
isa<MaterializeTemporaryExpr>(AE) || isa<CXXConstructExpr>(AE) ||
isa<InitListExpr>(AE) || isa<ImplicitCastExpr>(AE) ||
isa<CXXBindTemporaryExpr>(AE);
if (!LooksLikeTemp)
continue;

// Check if this record type has smart pointer fields
const auto *CRD = T->getAsCXXRecordDecl();
if (CRD) {
for (const FieldDecl *FD : CRD->fields()) {
if (isSmartOwningPtrType(FD->getType())) {
hasByValueRecordWithSmartPtr = true;
break;
}
}
}
if (hasByValueRecordWithSmartPtr)
break;
}

if (hasByValueRecordWithSmartPtr) {
ProgramStateRef State = C.getState();
RegionStateTy RS = State->get<RegionState>();
ProgramStateRef NewState = State;
for (auto [Sym, RefSt] : RS) {
if (RefSt.isAllocated() || RefSt.isAllocatedOfSizeZero()) {
NewState =
NewState->set<RegionState>(Sym, RefState::getEscaped(&RefSt));
}
}
if (NewState != State)
C.addTransition(NewState);
}
}
}
}

Expand Down
37 changes: 37 additions & 0 deletions clang/test/Analysis/NewDeleteLeaks-PR60896-shared.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// RUN: %clang_analyze_cc1 -analyzer-checker=core,cplusplus,unix -verify %s
// expected-no-diagnostics

#include "Inputs/system-header-simulator-for-malloc.h"

// Test shared_ptr support in the same pattern as the original PR60896 test
namespace shared_ptr_test {

template <typename T>
struct shared_ptr {
T* ptr;
shared_ptr(T* p) : ptr(p) {}
~shared_ptr() { delete ptr; }
shared_ptr(shared_ptr&& other) : ptr(other.ptr) { other.ptr = nullptr; }
T* get() const { return ptr; }
};

template <typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args) {
return shared_ptr<T>(new T(args...));
}

struct Foo {
shared_ptr<int> i;
};

void add(Foo foo) {
// The shared_ptr destructor will be called when foo goes out of scope
}

void test() {
// No warning should be emitted for this - the memory is managed by shared_ptr
// in the temporary Foo object, which will properly clean up the memory
add({make_shared<int>(1)});
}

} // namespace shared_ptr_test
44 changes: 44 additions & 0 deletions clang/test/Analysis/NewDeleteLeaks-PR60896.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// RUN: %clang_analyze_cc1 -verify -analyzer-output=text %s \
// RUN: -analyzer-checker=core \
// RUN: -analyzer-checker=cplusplus \
// RUN: -analyzer-checker=unix
// expected-no-diagnostics

#include "Inputs/system-header-simulator-for-malloc.h"

//===----------------------------------------------------------------------===//
// Check that we don't report leaks for unique_ptr in temporary objects
//===----------------------------------------------------------------------===//
namespace unique_ptr_temporary_PR60896 {

// We use a custom implementation of unique_ptr for testing purposes
template <typename T>
struct unique_ptr {
T* ptr;
unique_ptr(T* p) : ptr(p) {}
~unique_ptr() { delete ptr; }
unique_ptr(unique_ptr&& other) : ptr(other.ptr) { other.ptr = nullptr; }
T* get() const { return ptr; }
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure some header simulator already has a unique_ptr somewhere. We should use that.

Copy link
Contributor Author

@ivanmurashko ivanmurashko Aug 9, 2025

Choose a reason for hiding this comment

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

I looked at system-header-simulator-cxx.h — it has unique_ptr, but without a destructor implementation. For correctness, I kept the custom unique_ptr in the test to ensure proper leak modeling.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a proper destructor to it?


template <typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(args...));
}

// The test case that demonstrates the issue
struct Foo {
unique_ptr<int> i;
};

void add(Foo foo) {
// The unique_ptr destructor will be called when foo goes out of scope
}

void test() {
// No warning should be emitted for this - the memory is managed by unique_ptr
// in the temporary Foo object, which will properly clean up the memory
add({make_unique<int>(1)});
}

} // namespace unique_ptr_temporary_PR60896