Skip to content

Commit fc4b09d

Browse files
authored
[analyzer] Add an ownership change visitor to StreamChecker (#94957)
This is very similar to https://reviews.llvm.org/D105553, in fact, I barely made any changes from MallocChecker's ownership visitor to this one. The new visitor emits a diagnostic note for function where a change in stream ownership was expected (for example, it had a fclose() call), but the ownership remained unchanged. This is similar to messages regarding ordinary values ("Returning without writing to x").
1 parent 6ecb9fd commit fc4b09d

File tree

3 files changed

+273
-2
lines changed

3 files changed

+273
-2
lines changed

clang/lib/StaticAnalyzer/Checkers/NoOwnershipChangeVisitor.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ using namespace clang;
1818
using namespace ento;
1919
using OwnerSet = NoOwnershipChangeVisitor::OwnerSet;
2020

21+
namespace {
2122
// Collect which entities point to the allocated memory, and could be
2223
// responsible for deallocating it.
2324
class OwnershipBindingsHandler : public StoreManager::BindingsHandler {
@@ -46,6 +47,7 @@ class OwnershipBindingsHandler : public StoreManager::BindingsHandler {
4647
out << "}\n";
4748
}
4849
};
50+
} // namespace
4951

5052
OwnerSet NoOwnershipChangeVisitor::getOwnersAtNode(const ExplodedNode *N) {
5153
OwnerSet Ret;

clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
#include "NoOwnershipChangeVisitor.h"
14+
#include "clang/ASTMatchers/ASTMatchFinder.h"
15+
#include "clang/ASTMatchers/ASTMatchers.h"
1316
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
1417
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
1518
#include "clang/StaticAnalyzer/Core/Checker.h"
@@ -74,6 +77,12 @@ struct StreamErrorState {
7477
/// Returns if the StreamErrorState is a valid object.
7578
operator bool() const { return NoError || FEof || FError; }
7679

80+
LLVM_DUMP_METHOD void dump() const { dumpToStream(llvm::errs()); }
81+
LLVM_DUMP_METHOD void dumpToStream(llvm::raw_ostream &os) const {
82+
os << "NoError: " << NoError << ", FEof: " << FEof
83+
<< ", FError: " << FError;
84+
}
85+
7786
void Profile(llvm::FoldingSetNodeID &ID) const {
7887
ID.AddBoolean(NoError);
7988
ID.AddBoolean(FEof);
@@ -98,6 +107,18 @@ struct StreamState {
98107
OpenFailed /// The last open operation has failed.
99108
} State;
100109

110+
StringRef getKindStr() const {
111+
switch (State) {
112+
case Opened:
113+
return "Opened";
114+
case Closed:
115+
return "Closed";
116+
case OpenFailed:
117+
return "OpenFailed";
118+
}
119+
llvm_unreachable("Unknown StreamState!");
120+
}
121+
101122
/// State of the error flags.
102123
/// Ignored in non-opened stream state but must be NoError.
103124
StreamErrorState const ErrorState;
@@ -146,6 +167,9 @@ struct StreamState {
146167
return StreamState{L, OpenFailed, {}, false};
147168
}
148169

170+
LLVM_DUMP_METHOD void dump() const { dumpToStream(llvm::errs()); }
171+
LLVM_DUMP_METHOD void dumpToStream(llvm::raw_ostream &os) const;
172+
149173
void Profile(llvm::FoldingSetNodeID &ID) const {
150174
ID.AddPointer(LastOperation);
151175
ID.AddInteger(State);
@@ -183,6 +207,14 @@ struct FnDescription {
183207
ArgNoTy StreamArgNo;
184208
};
185209

210+
LLVM_DUMP_METHOD void StreamState::dumpToStream(llvm::raw_ostream &os) const {
211+
os << "{Kind: " << getKindStr() << ", Last operation: " << LastOperation
212+
<< ", ErrorState: ";
213+
ErrorState.dumpToStream(os);
214+
os << ", FilePos: " << (FilePositionIndeterminate ? "Indeterminate" : "OK")
215+
<< '}';
216+
}
217+
186218
/// Get the value of the stream argument out of the passed call event.
187219
/// The call should contain a function that is described by Desc.
188220
SVal getStreamArg(const FnDescription *Desc, const CallEvent &Call) {
@@ -300,6 +332,8 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
300332
/// If true, generate failure branches for cases that are often not checked.
301333
bool PedanticMode = false;
302334

335+
const CallDescription FCloseDesc = {CDM::CLibrary, {"fclose"}, 1};
336+
303337
private:
304338
CallDescriptionMap<FnDescription> FnDescriptions = {
305339
{{CDM::CLibrary, {"fopen"}, 2},
@@ -310,8 +344,7 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
310344
{&StreamChecker::preFreopen, &StreamChecker::evalFreopen, 2}},
311345
{{CDM::CLibrary, {"tmpfile"}, 0},
312346
{nullptr, &StreamChecker::evalFopen, ArgNone}},
313-
{{CDM::CLibrary, {"fclose"}, 1},
314-
{&StreamChecker::preDefault, &StreamChecker::evalFclose, 0}},
347+
{FCloseDesc, {&StreamChecker::preDefault, &StreamChecker::evalFclose, 0}},
315348
{{CDM::CLibrary, {"fread"}, 4},
316349
{&StreamChecker::preRead,
317350
std::bind(&StreamChecker::evalFreadFwrite, _1, _2, _3, _4, true), 3}},
@@ -696,6 +729,62 @@ struct StreamOperationEvaluator {
696729

697730
} // end anonymous namespace
698731

732+
//===----------------------------------------------------------------------===//
733+
// Definition of NoStreamStateChangeVisitor.
734+
//===----------------------------------------------------------------------===//
735+
736+
namespace {
737+
class NoStreamStateChangeVisitor final : public NoOwnershipChangeVisitor {
738+
protected:
739+
/// Syntactically checks whether the callee is a closing function. Since
740+
/// we have no path-sensitive information on this call (we would need a
741+
/// CallEvent instead of a CallExpr for that), its possible that a
742+
/// closing function was called indirectly through a function pointer,
743+
/// but we are not able to tell, so this is a best effort analysis.
744+
bool isClosingCallAsWritten(const CallExpr &Call) const {
745+
const auto *StreamChk = static_cast<const StreamChecker *>(&Checker);
746+
return StreamChk->FCloseDesc.matchesAsWritten(Call);
747+
}
748+
749+
bool doesFnIntendToHandleOwnership(const Decl *Callee,
750+
ASTContext &ACtx) final {
751+
using namespace clang::ast_matchers;
752+
const FunctionDecl *FD = dyn_cast<FunctionDecl>(Callee);
753+
754+
auto Matches =
755+
match(findAll(callExpr().bind("call")), *FD->getBody(), ACtx);
756+
for (BoundNodes Match : Matches) {
757+
if (const auto *Call = Match.getNodeAs<CallExpr>("call"))
758+
if (isClosingCallAsWritten(*Call))
759+
return true;
760+
}
761+
// TODO: Ownership might change with an attempt to store stream object, not
762+
// only through closing it. Check for attempted stores as well.
763+
return false;
764+
}
765+
766+
bool hasResourceStateChanged(ProgramStateRef CallEnterState,
767+
ProgramStateRef CallExitEndState) final {
768+
return CallEnterState->get<StreamMap>(Sym) !=
769+
CallExitEndState->get<StreamMap>(Sym);
770+
}
771+
772+
PathDiagnosticPieceRef emitNote(const ExplodedNode *N) override {
773+
PathDiagnosticLocation L = PathDiagnosticLocation::create(
774+
N->getLocation(),
775+
N->getState()->getStateManager().getContext().getSourceManager());
776+
return std::make_shared<PathDiagnosticEventPiece>(
777+
L, "Returning without closing stream object or storing it for later "
778+
"release");
779+
}
780+
781+
public:
782+
NoStreamStateChangeVisitor(SymbolRef Sym, const StreamChecker *Checker)
783+
: NoOwnershipChangeVisitor(Sym, Checker) {}
784+
};
785+
786+
} // end anonymous namespace
787+
699788
const ExplodedNode *StreamChecker::getAcquisitionSite(const ExplodedNode *N,
700789
SymbolRef StreamSym,
701790
CheckerContext &C) {
@@ -1872,6 +1961,7 @@ StreamChecker::reportLeaks(const SmallVector<SymbolRef, 2> &LeakedSyms,
18721961
LocUsedForUniqueing,
18731962
StreamOpenNode->getLocationContext()->getDecl());
18741963
R->markInteresting(LeakSym);
1964+
R->addVisitor<NoStreamStateChangeVisitor>(LeakSym, this);
18751965
C.emitReport(std::move(R));
18761966
}
18771967

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// RUN: %clang_analyze_cc1 -verify %s -analyzer-output=text \
2+
// RUN: -analyzer-checker=core \
3+
// RUN: -analyzer-checker=unix.Stream
4+
5+
6+
#include "Inputs/system-header-simulator.h"
7+
char *logDump();
8+
bool coin();
9+
10+
[[noreturn]] void halt();
11+
12+
void assert(bool b) {
13+
if (!b)
14+
halt();
15+
}
16+
17+
//===----------------------------------------------------------------------===//
18+
// Report for which we expect NoOwnershipChangeVisitor to add a new note.
19+
//===----------------------------------------------------------------------===//
20+
21+
namespace stream_opened_in_fn_call {
22+
// TODO: AST analysis of sink would reveal that it doesn't intent to free the
23+
// allocated memory, but in this instance, its also the only function with
24+
// the ability to do so, we should see a note here.
25+
void sink(FILE *f) {
26+
}
27+
28+
void f() {
29+
sink(fopen("input.txt", "w"));
30+
// expected-note@-1{{Stream opened here}}
31+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
32+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
33+
} // namespace stream_opened_in_fn_call
34+
35+
namespace stream_passed_to_fn_call {
36+
37+
void expectedClose(FILE *f) {
38+
if (char *log = logDump()) { // expected-note{{Assuming 'log' is null}}
39+
// expected-note@-1{{Taking false branch}}
40+
printf("%s", log);
41+
fclose(f);
42+
}
43+
} // expected-note{{Returning without closing stream object or storing it for later release}}
44+
45+
void f() {
46+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
47+
if (!f) // expected-note{{'f' is non-null}}
48+
// expected-note@-1{{Taking false branch}}
49+
return;
50+
if (coin()) { // expected-note{{Assuming the condition is true}}
51+
// expected-note@-1{{Taking true branch}}
52+
expectedClose(f); // expected-note{{Calling 'expectedClose'}}
53+
// expected-note@-1{{Returning from 'expectedClose'}}
54+
55+
return; // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
56+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
57+
}
58+
fclose(f);
59+
}
60+
} // namespace stream_passed_to_fn_call
61+
62+
namespace stream_shared_with_ptr_of_shorter_lifetime {
63+
64+
void sink(FILE *f) {
65+
FILE *Q = f;
66+
if (coin()) // expected-note {{Assuming the condition is false}}
67+
// expected-note@-1 {{Taking false branch}}
68+
fclose(f);
69+
(void)Q;
70+
} // expected-note{{Returning without closing stream object or storing it for later release}}
71+
72+
void foo() {
73+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
74+
if (!f) // expected-note{{'f' is non-null}}
75+
// expected-note@-1{{Taking false branch}}
76+
return;
77+
sink(f); // expected-note {{Calling 'sink'}}
78+
// expected-note@-1 {{Returning from 'sink'}}
79+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
80+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
81+
82+
} // namespace stream_shared_with_ptr_of_shorter_lifetime
83+
84+
//===----------------------------------------------------------------------===//
85+
// Report for which we *do not* expect NoOwnershipChangeVisitor add a new note,
86+
// nor do we want it to.
87+
//===----------------------------------------------------------------------===//
88+
89+
namespace stream_not_passed_to_fn_call {
90+
91+
void expectedClose(FILE *f) {
92+
if (char *log = logDump()) {
93+
printf("%s", log);
94+
fclose(f);
95+
}
96+
}
97+
98+
void f(FILE *p) {
99+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
100+
if (!f) // expected-note{{'f' is non-null}}
101+
// expected-note@-1{{Taking false branch}}
102+
return;
103+
expectedClose(p); // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
104+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
105+
}
106+
} // namespace stream_not_passed_to_fn_call
107+
108+
namespace stream_shared_with_ptr_of_same_lifetime {
109+
110+
void expectedClose(FILE *f, FILE **p) {
111+
// NOTE: Not a job of NoOwnershipChangeVisitor, but maybe this could be
112+
// highlighted still?
113+
*p = f;
114+
}
115+
116+
void f() {
117+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
118+
FILE *p = NULL;
119+
if (!f) // expected-note{{'f' is non-null}}
120+
// expected-note@-1{{Taking false branch}}
121+
return;
122+
expectedClose(f, &p);
123+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
124+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
125+
} // namespace stream_shared_with_ptr_of_same_lifetime
126+
127+
namespace stream_passed_into_fn_that_doesnt_intend_to_free {
128+
void expectedClose(FILE *f) {
129+
}
130+
131+
void f() {
132+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
133+
if (!f) // expected-note{{'f' is non-null}}
134+
// expected-note@-1{{Taking false branch}}
135+
return;
136+
expectedClose(f);
137+
138+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
139+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
140+
} // namespace stream_passed_into_fn_that_doesnt_intend_to_free
141+
142+
namespace stream_passed_into_fn_that_doesnt_intend_to_free2 {
143+
void bar();
144+
145+
void expectedClose(FILE *f) {
146+
// Correctly realize that calling bar() doesn't mean that this function would
147+
// like to deallocate anything.
148+
bar();
149+
}
150+
151+
void f() {
152+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
153+
if (!f) // expected-note{{'f' is non-null}}
154+
// expected-note@-1{{Taking false branch}}
155+
return;
156+
expectedClose(f);
157+
158+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
159+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
160+
} // namespace stream_passed_into_fn_that_doesnt_intend_to_free2
161+
162+
namespace streamstate_from_closed_to_open {
163+
164+
// StreamState of the symbol changed from nothing to Allocated. We don't want to
165+
// emit notes when the RefKind changes in the stack frame.
166+
static FILE *fopenWrapper() {
167+
FILE *f = fopen("input.txt", "w"); // expected-note{{Stream opened here}}
168+
assert(f);
169+
return f;
170+
}
171+
void use_ret() {
172+
FILE *v;
173+
v = fopenWrapper(); // expected-note {{Calling 'fopenWrapper'}}
174+
// expected-note@-1{{Returning from 'fopenWrapper'}}
175+
176+
} // expected-warning{{Opened stream never closed. Potential resource leak [unix.Stream]}}
177+
// expected-note@-1{{Opened stream never closed. Potential resource leak}}
178+
179+
} // namespace streamstate_from_closed_to_open

0 commit comments

Comments
 (0)