Skip to content

Commit 2d3668c

Browse files
committed
[analyzer] MallocChecker: Add a visitor to leave a note on functions that could have, but did not change ownership on leaked memory
This is a rather common feedback we get from out leak checkers: bug reports are really short, and are contain barely any usable information on what the analyzer did to conclude that a leak actually happened. This happens because of our bug report minimizing effort. We construct bug reports by inspecting the ExplodedNodes that lead to the error from the bottom up (from the error node all the way to the root of the exploded graph), and mark entities that were the cause of a bug, or have interacted with it as interesting. In order to make the bug report a bit less verbose, whenever we find an entire function call (from CallEnter to CallExitEnd) that didn't talk about any interesting entity, we prune it (click here for more info on bug report generation). Even if the event to highlight is exactly this lack of interaction with interesting entities. D105553 generalized the visitor that creates notes for these cases. This patch adds a new kind of NoStateChangeVisitor that leaves notes in functions that took a piece of dynamically allocated memory that later leaked as parameter, and didn't change its ownership status. Differential Revision: https://reviews.llvm.org/D105553
1 parent 54a61c9 commit 2d3668c

File tree

4 files changed

+300
-2
lines changed

4 files changed

+300
-2
lines changed

clang/include/clang/StaticAnalyzer/Checkers/Checkers.td

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,17 @@ def DynamicMemoryModeling: Checker<"DynamicMemoryModeling">,
485485
"allocating and deallocating functions are annotated with "
486486
"ownership_holds, ownership_takes and ownership_returns.",
487487
"false",
488-
InAlpha>
488+
InAlpha>,
489+
CmdLineOption<Boolean,
490+
"AddNoOwnershipChangeNotes",
491+
"Add an additional note to the bug report for leak-like "
492+
"bugs. Dynamically allocated objects passed to functions "
493+
"that neither deallocated it, or have taken responsibility "
494+
"of the ownership are noted, similarly to "
495+
"NoStoreFuncVisitor.",
496+
"false",
497+
InAlpha,
498+
Hide>
489499
]>,
490500
Dependencies<[CStringModeling]>,
491501
Documentation<NotDocumented>,

clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "InterCheckerAPI.h"
4949
#include "clang/AST/Attr.h"
5050
#include "clang/AST/DeclCXX.h"
51+
#include "clang/AST/DeclTemplate.h"
5152
#include "clang/AST/Expr.h"
5253
#include "clang/AST/ExprCXX.h"
5354
#include "clang/AST/ParentMap.h"
@@ -64,12 +65,15 @@
6465
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
6566
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h"
6667
#include "clang/StaticAnalyzer/Core/PathSensitive/DynamicExtent.h"
68+
#include "clang/StaticAnalyzer/Core/PathSensitive/ExplodedGraph.h"
6769
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
6870
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
6971
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState_Fwd.h"
7072
#include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h"
73+
#include "clang/StaticAnalyzer/Core/PathSensitive/StoreRef.h"
7174
#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
7275
#include "llvm/ADT/STLExtras.h"
76+
#include "llvm/ADT/SetOperations.h"
7377
#include "llvm/ADT/SmallString.h"
7478
#include "llvm/ADT/StringExtras.h"
7579
#include "llvm/Support/Compiler.h"
@@ -298,6 +302,8 @@ class MallocChecker
298302
/// which might free a pointer are annotated.
299303
DefaultBool ShouldIncludeOwnershipAnnotatedFunctions;
300304

305+
DefaultBool ShouldRegisterNoOwnershipChangeVisitor;
306+
301307
/// Many checkers are essentially built into this one, so enabling them will
302308
/// make MallocChecker perform additional modeling and reporting.
303309
enum CheckKind {
@@ -722,11 +728,146 @@ class MallocChecker
722728
bool isArgZERO_SIZE_PTR(ProgramStateRef State, CheckerContext &C,
723729
SVal ArgVal) const;
724730
};
731+
} // end anonymous namespace
732+
733+
//===----------------------------------------------------------------------===//
734+
// Definition of NoOwnershipChangeVisitor.
735+
//===----------------------------------------------------------------------===//
736+
737+
namespace {
738+
class NoOwnershipChangeVisitor final : public NoStateChangeFuncVisitor {
739+
SymbolRef Sym;
740+
using OwnerSet = llvm::SmallPtrSet<const MemRegion *, 8>;
741+
742+
// Collect which entities point to the allocated memory, and could be
743+
// responsible for deallocating it.
744+
class OwnershipBindingsHandler : public StoreManager::BindingsHandler {
745+
SymbolRef Sym;
746+
OwnerSet &Owners;
747+
748+
public:
749+
OwnershipBindingsHandler(SymbolRef Sym, OwnerSet &Owners)
750+
: Sym(Sym), Owners(Owners) {}
751+
752+
bool HandleBinding(StoreManager &SMgr, Store Store, const MemRegion *Region,
753+
SVal Val) override {
754+
if (Val.getAsSymbol() == Sym)
755+
Owners.insert(Region);
756+
return true;
757+
}
758+
};
759+
760+
protected:
761+
OwnerSet getOwnersAtNode(const ExplodedNode *N) {
762+
OwnerSet Ret;
763+
764+
ProgramStateRef State = N->getState();
765+
OwnershipBindingsHandler Handler{Sym, Ret};
766+
State->getStateManager().getStoreManager().iterBindings(State->getStore(),
767+
Handler);
768+
return Ret;
769+
}
770+
771+
static const ExplodedNode *getCallExitEnd(const ExplodedNode *N) {
772+
while (N && !N->getLocationAs<CallExitEnd>())
773+
N = N->getFirstSucc();
774+
return N;
775+
}
776+
777+
virtual bool
778+
wasModifiedBeforeCallExit(const ExplodedNode *CurrN,
779+
const ExplodedNode *CallExitN) override {
780+
if (CurrN->getLocationAs<CallEnter>())
781+
return true;
782+
783+
// Its the state right *after* the call that is interesting. Any pointers
784+
// inside the call that pointed to the allocated memory are of little
785+
// consequence if their lifetime ends within the function.
786+
CallExitN = getCallExitEnd(CallExitN);
787+
if (!CallExitN)
788+
return true;
789+
790+
if (CurrN->getState()->get<RegionState>(Sym) !=
791+
CallExitN->getState()->get<RegionState>(Sym))
792+
return true;
793+
794+
OwnerSet CurrOwners = getOwnersAtNode(CurrN);
795+
OwnerSet ExitOwners = getOwnersAtNode(CallExitN);
796+
797+
// Owners in the current set may be purged from the analyzer later on.
798+
// If a variable is dead (is not referenced directly or indirectly after
799+
// some point), it will be removed from the Store before the end of its
800+
// actual lifetime.
801+
// This means that that if the ownership status didn't change, CurrOwners
802+
// must be a superset of, but not necessarily equal to ExitOwners.
803+
return !llvm::set_is_subset(ExitOwners, CurrOwners);
804+
}
805+
806+
static PathDiagnosticPieceRef emitNote(const ExplodedNode *N) {
807+
PathDiagnosticLocation L = PathDiagnosticLocation::create(
808+
N->getLocation(),
809+
N->getState()->getStateManager().getContext().getSourceManager());
810+
return std::make_shared<PathDiagnosticEventPiece>(
811+
L, "Returning without deallocating memory or storing the pointer for "
812+
"later deallocation");
813+
}
814+
815+
virtual PathDiagnosticPieceRef
816+
maybeEmitNoteForObjCSelf(PathSensitiveBugReport &R,
817+
const ObjCMethodCall &Call,
818+
const ExplodedNode *N) override {
819+
// TODO: Implement.
820+
return nullptr;
821+
}
822+
823+
virtual PathDiagnosticPieceRef
824+
maybeEmitNoteForCXXThis(PathSensitiveBugReport &R,
825+
const CXXConstructorCall &Call,
826+
const ExplodedNode *N) override {
827+
// TODO: Implement.
828+
return nullptr;
829+
}
830+
831+
virtual PathDiagnosticPieceRef
832+
maybeEmitNoteForParameters(PathSensitiveBugReport &R, const CallEvent &Call,
833+
const ExplodedNode *N) override {
834+
// TODO: Factor the logic of "what constitutes as an entity being passed
835+
// into a function call" out by reusing the code in
836+
// NoStoreFuncVisitor::maybeEmitNoteForParameters, maybe by incorporating
837+
// the printing technology in UninitializedObject's FieldChainInfo.
838+
ArrayRef<ParmVarDecl *> Parameters = Call.parameters();
839+
for (unsigned I = 0; I < Call.getNumArgs() && I < Parameters.size(); ++I) {
840+
SVal V = Call.getArgSVal(I);
841+
if (V.getAsSymbol() == Sym)
842+
return emitNote(N);
843+
}
844+
return nullptr;
845+
}
846+
847+
public:
848+
NoOwnershipChangeVisitor(SymbolRef Sym)
849+
: NoStateChangeFuncVisitor(bugreporter::TrackingKind::Thorough),
850+
Sym(Sym) {}
851+
852+
void Profile(llvm::FoldingSetNodeID &ID) const override {
853+
static int Tag = 0;
854+
ID.AddPointer(&Tag);
855+
ID.AddPointer(Sym);
856+
}
857+
858+
void *getTag() const {
859+
static int Tag = 0;
860+
return static_cast<void *>(&Tag);
861+
}
862+
};
863+
864+
} // end anonymous namespace
725865

726866
//===----------------------------------------------------------------------===//
727867
// Definition of MallocBugVisitor.
728868
//===----------------------------------------------------------------------===//
729869

870+
namespace {
730871
/// The bug visitor which allows us to print extra diagnostics along the
731872
/// BugReport path. For example, showing the allocation site of the leaked
732873
/// region.
@@ -851,7 +992,6 @@ class MallocBugVisitor final : public BugReporterVisitor {
851992
}
852993
};
853994
};
854-
855995
} // end anonymous namespace
856996

857997
// A map from the freed symbol to the symbol representing the return value of
@@ -2579,6 +2719,8 @@ void MallocChecker::HandleLeak(SymbolRef Sym, ExplodedNode *N,
25792719
AllocNode->getLocationContext()->getDecl());
25802720
R->markInteresting(Sym);
25812721
R->addVisitor<MallocBugVisitor>(Sym, true);
2722+
if (ShouldRegisterNoOwnershipChangeVisitor)
2723+
R->addVisitor<NoOwnershipChangeVisitor>(Sym);
25822724
C.emitReport(std::move(R));
25832725
}
25842726

@@ -3395,6 +3537,9 @@ void ento::registerDynamicMemoryModeling(CheckerManager &mgr) {
33953537
auto *checker = mgr.registerChecker<MallocChecker>();
33963538
checker->ShouldIncludeOwnershipAnnotatedFunctions =
33973539
mgr.getAnalyzerOptions().getCheckerBooleanOption(checker, "Optimistic");
3540+
checker->ShouldRegisterNoOwnershipChangeVisitor =
3541+
mgr.getAnalyzerOptions().getCheckerBooleanOption(
3542+
checker, "AddNoOwnershipChangeNotes");
33983543
}
33993544

34003545
bool ento::shouldRegisterDynamicMemoryModeling(const CheckerManager &mgr) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// RUN: %clang_analyze_cc1 -verify -analyzer-output=text %s \
2+
// RUN: -analyzer-checker=core \
3+
// RUN: -analyzer-checker=cplusplus \
4+
// RUN: -analyzer-checker=unix \
5+
// RUN: -analyzer-config \
6+
// RUN: unix.DynamicMemoryModeling:AddNoOwnershipChangeNotes=false
7+
8+
// RUN: %clang_analyze_cc1 -verify=expected,ownership -analyzer-output=text %s \
9+
// RUN: -analyzer-checker=core \
10+
// RUN: -analyzer-checker=cplusplus \
11+
// RUN: -analyzer-checker=unix \
12+
// RUN: -analyzer-config \
13+
// RUN: unix.DynamicMemoryModeling:AddNoOwnershipChangeNotes=true
14+
15+
#include "Inputs/system-header-simulator-for-malloc.h"
16+
17+
//===----------------------------------------------------------------------===//
18+
// Report for which we expect NoOwnershipChangeVisitor to add a new note.
19+
//===----------------------------------------------------------------------===//
20+
21+
bool coin();
22+
23+
namespace memory_allocated_in_fn_call {
24+
25+
void sink(int *P) {
26+
} // ownership-note {{Returning without deallocating memory or storing the pointer for later deallocation}}
27+
28+
void foo() {
29+
sink(new int(5)); // expected-note {{Memory is allocated}}
30+
// ownership-note@-1 {{Calling 'sink'}}
31+
// ownership-note@-2 {{Returning from 'sink'}}
32+
} // expected-warning {{Potential memory leak [cplusplus.NewDeleteLeaks]}}
33+
// expected-note@-1 {{Potential memory leak}}
34+
35+
} // namespace memory_allocated_in_fn_call
36+
37+
namespace memory_passed_to_fn_call {
38+
39+
void sink(int *P) {
40+
if (coin()) // ownership-note {{Assuming the condition is false}}
41+
// ownership-note@-1 {{Taking false branch}}
42+
delete P;
43+
} // ownership-note {{Returning without deallocating memory or storing the pointer for later deallocation}}
44+
45+
void foo() {
46+
int *ptr = new int(5); // expected-note {{Memory is allocated}}
47+
sink(ptr); // ownership-note {{Calling 'sink'}}
48+
// ownership-note@-1 {{Returning from 'sink'}}
49+
} // expected-warning {{Potential leak of memory pointed to by 'ptr' [cplusplus.NewDeleteLeaks]}}
50+
// expected-note@-1 {{Potential leak}}
51+
52+
} // namespace memory_passed_to_fn_call
53+
54+
namespace memory_shared_with_ptr_of_shorter_lifetime {
55+
56+
void sink(int *P) {
57+
int *Q = P;
58+
if (coin()) // ownership-note {{Assuming the condition is false}}
59+
// ownership-note@-1 {{Taking false branch}}
60+
delete P;
61+
(void)Q;
62+
} // ownership-note {{Returning without deallocating memory or storing the pointer for later deallocation}}
63+
64+
void foo() {
65+
int *ptr = new int(5); // expected-note {{Memory is allocated}}
66+
sink(ptr); // ownership-note {{Calling 'sink'}}
67+
// ownership-note@-1 {{Returning from 'sink'}}
68+
} // expected-warning {{Potential leak of memory pointed to by 'ptr' [cplusplus.NewDeleteLeaks]}}
69+
// expected-note@-1 {{Potential leak}}
70+
71+
} // namespace memory_shared_with_ptr_of_shorter_lifetime
72+
73+
//===----------------------------------------------------------------------===//
74+
// Report for which we *do not* expect NoOwnershipChangeVisitor add a new note,
75+
// nor do we want it to.
76+
//===----------------------------------------------------------------------===//
77+
78+
namespace memory_not_passed_to_fn_call {
79+
80+
void sink(int *P) {
81+
if (coin())
82+
delete P;
83+
}
84+
85+
void foo() {
86+
int *ptr = new int(5); // expected-note {{Memory is allocated}}
87+
int *q = nullptr;
88+
sink(q);
89+
(void)ptr;
90+
} // expected-warning {{Potential leak of memory pointed to by 'ptr' [cplusplus.NewDeleteLeaks]}}
91+
// expected-note@-1 {{Potential leak}}
92+
93+
} // namespace memory_not_passed_to_fn_call
94+
95+
namespace memory_shared_with_ptr_of_same_lifetime {
96+
97+
void sink(int *P, int **Q) {
98+
// NOTE: Not a job of NoOwnershipChangeVisitor, but maybe this could be
99+
// highlighted still?
100+
*Q = P;
101+
}
102+
103+
void foo() {
104+
int *ptr = new int(5); // expected-note {{Memory is allocated}}
105+
int *q = nullptr;
106+
sink(ptr, &q);
107+
} // expected-warning {{Potential leak of memory pointed to by 'q' [cplusplus.NewDeleteLeaks]}}
108+
// expected-note@-1 {{Potential leak}}
109+
110+
} // namespace memory_shared_with_ptr_of_same_lifetime
111+
112+
// TODO: We don't want a note here. sink() doesn't seem like a function that
113+
// even attempts to take care of any memory ownership problems.
114+
namespace memory_passed_into_fn_that_doesnt_intend_to_free {
115+
116+
void sink(int *P) {
117+
} // ownership-note {{Returning without deallocating memory or storing the pointer for later deallocation}}
118+
119+
void foo() {
120+
int *ptr = new int(5); // expected-note {{Memory is allocated}}
121+
sink(ptr); // ownership-note {{Calling 'sink'}}
122+
// ownership-note@-1 {{Returning from 'sink'}}
123+
} // expected-warning {{Potential leak of memory pointed to by 'ptr' [cplusplus.NewDeleteLeaks]}}
124+
// expected-note@-1 {{Potential leak}}
125+
126+
} // namespace memory_passed_into_fn_that_doesnt_intend_to_free
127+
128+
namespace refkind_from_unoallocated_to_allocated {
129+
130+
// RefKind of the symbol changed from nothing to Allocated. We don't want to
131+
// emit notes when the RefKind changes in the stack frame.
132+
static char *malloc_wrapper_ret() {
133+
return (char *)malloc(12); // expected-note {{Memory is allocated}}
134+
}
135+
void use_ret() {
136+
char *v;
137+
v = malloc_wrapper_ret(); // expected-note {{Calling 'malloc_wrapper_ret'}}
138+
// expected-note@-1 {{Returned allocated memory}}
139+
} // expected-warning {{Potential leak of memory pointed to by 'v' [unix.Malloc]}}
140+
// expected-note@-1 {{Potential leak of memory pointed to by 'v'}}
141+
142+
} // namespace refkind_from_unoallocated_to_allocated

clang/test/Analysis/analyzer-config.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
// CHECK-NEXT: suppress-null-return-paths = true
117117
// CHECK-NEXT: track-conditions = true
118118
// CHECK-NEXT: track-conditions-debug = false
119+
// CHECK-NEXT: unix.DynamicMemoryModeling:AddNoOwnershipChangeNotes = false
119120
// CHECK-NEXT: unix.DynamicMemoryModeling:Optimistic = false
120121
// CHECK-NEXT: unroll-loops = false
121122
// CHECK-NEXT: widen-loops = false

0 commit comments

Comments
 (0)