254 changes: 254 additions & 0 deletions lib/StaticAnalyzer/Checkers/SimpleStreamCheckerV2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//===-- SimpleStreamChecker.cpp -----------------------------------------*- C++ -*--//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// Defines a checker for proper use of fopen/fclose APIs.
// - If a file has been closed with fclose, it should not be accessed again.
// Accessing a closed file results in undefined behavior.
// - If a file was opened with fopen, it must be closed with fclose before
// the execution ends. Failing to do so results in a resource leak.
//
//===----------------------------------------------------------------------===//

#include "ClangSACheckers.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"

using namespace clang;
using namespace ento;

namespace {
enum SimpleStreamState { Closed, Opened };
SmartStateTrait SimpleStreamTrait;
typedef SmallVector<SymbolRef, 2> SymbolVector;

class SimpleStreamModel
: public Checker<check::ASTDecl<TranslationUnitDecl>, check::PostCall,
check::PointerEscape> {
CallDescription OpenFn, CloseFn;
bool guaranteedNotToCloseFile(const CallEvent &Call) const;

public:
SimpleStreamModel() : OpenFn("fopen"), CloseFn("fclose") {}

void checkASTDecl(const TranslationUnitDecl *D, AnalysisManager &AMgr,
BugReporter &BR) const;
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
ProgramStateRef checkPointerEscape(ProgramStateRef State,
const InvalidatedSymbols &Escaped,
const CallEvent *Call,
PointerEscapeKind Kind) const;
};
} // end of anonymous namespace

bool SimpleStreamModel::guaranteedNotToCloseFile(
const CallEvent &Call) const {
// If it's not in a system header, assume it might close a file.
if (!Call.isInSystemHeader())
return false;

// Handle cases where we know a buffer's /address/ can escape.
if (Call.argumentsMayEscape())
return false;

// Note, even though fclose closes the file, we do not list it here
// since the checker is modeling the call.
return true;
}

void SimpleStreamModel::checkASTDecl(const TranslationUnitDecl *D,
AnalysisManager &AMgr,
BugReporter &BR) const {
// Once the AST context is available, we can express the fact that our trait
// has type 'int'.
// FIXME: This is ugly. Maybe make a separate callback, or delay registering
// checkers until AST is constructed?
SimpleStreamTrait.initialize("SimpleStream", AMgr.getASTContext().IntTy);
}

void SimpleStreamModel::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {
if (!Call.isGlobalCFunction())
return;

if (Call.isCalled(OpenFn)) {

// Get the symbolic value corresponding to the file handle.
SymbolRef FileDesc = Call.getReturnValue().getAsSymbol();
if (!FileDesc)
return;

// Generate the next transition (an edge in the exploded graph).
ProgramStateRef State = C.getState();
State = State->bindLoc(SimpleStreamTrait, FileDesc, Opened);
C.addTransition(State);

} else if (Call.isCalled(CloseFn)) {

// Get the symbolic value corresponding to the file handle.
SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
if (!FileDesc)
return;

// Generate the next transition, in which the stream is closed.
ProgramStateRef State = C.getState();
State = State->bindLoc(SimpleStreamTrait, FileDesc, Closed);
C.addTransition(State);
}
}

ProgramStateRef SimpleStreamModel::checkPointerEscape(
ProgramStateRef State, const InvalidatedSymbols &Escaped,
const CallEvent *Call, PointerEscapeKind Kind) const {
// If we know that the call cannot close a file, there is nothing to do.
if (Kind == PSK_DirectEscapeOnCall && guaranteedNotToCloseFile(*Call))
return State;

for (auto I = Escaped.begin(), E = Escaped.end(); I != E; ++I) {
SymbolRef Sym = *I;

// The symbol escaped. Optimistically, assume that the corresponding file
// handle will be closed somewhere else.
// FIXME: We're doing double lookup here, and we could have probably just
// deleted the binding, because our checker doesn't discriminate between
// different kind of unknowns.
if (State->hasAnyBinding(SimpleStreamTrait, Sym))
State = State->bindLoc(SimpleStreamTrait, Sym, UnknownVal());
}
return State;
}

namespace {
class SimpleStreamChecker
: public Checker<check::PreCall, check::DeadSymbols> {
CallDescription CloseFn;

std::unique_ptr<BugType> DoubleCloseBugType;
std::unique_ptr<BugType> LeakBugType;

void reportDoubleClose(SymbolRef FileDescSym,
const CallEvent &Call,
CheckerContext &C) const;

void reportLeaks(ArrayRef<SymbolRef> LeakedStreams, CheckerContext &C,
ExplodedNode *ErrNode) const;

public:
SimpleStreamChecker();

/// Process fclose.
void checkPreCall(const CallEvent &Call, CheckerContext &C) const;

/// Detect leaks.
void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const;
};
} // end anonymous namespace

SimpleStreamChecker::SimpleStreamChecker() : CloseFn("fclose", 1) {
// Initialize the bug types.
DoubleCloseBugType.reset(
new BugType(this, "Double fclose", "Unix Stream API Error"));

LeakBugType.reset(
new BugType(this, "Resource Leak", "Unix Stream API Error"));
// Sinks are higher importance bugs as well as calls to assert() or exit(0).
LeakBugType->setSuppressOnSink(true);
}

void SimpleStreamChecker::checkPreCall(const CallEvent &Call,
CheckerContext &C) const {
if (!Call.isGlobalCFunction())
return;

if (!Call.isCalled(CloseFn))
return;

// Get the symbolic value corresponding to the file handle.
SymbolRef FileDesc = Call.getArgSVal(0).getAsSymbol();
if (!FileDesc)
return;


// Check if the stream has already been closed.
ProgramStateRef State = C.getState();
SVal Val = State->getSVal(SimpleStreamTrait, FileDesc);
if (Val.isConstant(Closed)) {
reportDoubleClose(FileDesc, Call, C);
return;
}
}

static bool isLeaked(SymbolRef Sym, ProgramStateRef State) {
// If a symbol is NULL, assume that fopen failed on this path.
// A symbol should only be considered leaked if it is non-null.
ConstraintManager &CMgr = State->getConstraintManager();
ConditionTruthVal OpenFailed = CMgr.isNull(State, Sym);
return !OpenFailed.isConstrainedTrue();
}

void SimpleStreamChecker::checkDeadSymbols(SymbolReaper &SymReaper,
CheckerContext &C) const {
ProgramStateRef State = C.getState();
SymbolVector LeakedStreams;
// FIXME: Iterate through trait, not through dead symbols (might be faster)?
for (auto I = SymReaper.dead_begin(), E = SymReaper.dead_end(); I != E; ++I) {
SymbolRef FileDesc = *I;
SVal Val = State->getSVal(SimpleStreamTrait, FileDesc);
if (!Val.isConstant(Opened))
continue;

// Collect leaked symbols.
if (isLeaked(FileDesc, State))
LeakedStreams.push_back(FileDesc);
}

if (LeakedStreams.empty())
return;

ExplodedNode *N = C.generateNonFatalErrorNode(State);
if (!N)
return;
reportLeaks(LeakedStreams, C, N);
}

void SimpleStreamChecker::reportDoubleClose(SymbolRef FileDescSym,
const CallEvent &Call,
CheckerContext &C) const {
// We reached a bug, stop exploring the path here by generating a sink.
ExplodedNode *ErrNode = C.generateErrorNode();
// If we've already reached this node on another path, return.
if (!ErrNode)
return;

// Generate the report.
auto R = llvm::make_unique<BugReport>(*DoubleCloseBugType,
"Closing a previously closed file stream", ErrNode);
R->addRange(Call.getSourceRange());
R->markInteresting(FileDescSym);
C.emitReport(std::move(R));
}

void SimpleStreamChecker::reportLeaks(ArrayRef<SymbolRef> LeakedStreams,
CheckerContext &C,
ExplodedNode *ErrNode) const {
// Attach bug reports to the leak node.
// TODO: Identify the leaked file descriptor.
for (SymbolRef LeakedStream : LeakedStreams) {
auto R = llvm::make_unique<BugReport>(*LeakBugType,
"Opened file is never closed; potential resource leak", ErrNode);
R->markInteresting(LeakedStream);
C.emitReport(std::move(R));
}
}

void ento::registerSimpleStreamCheckerV2(CheckerManager &mgr) {
mgr.registerChecker<SimpleStreamModel>();
mgr.registerChecker<SimpleStreamChecker>();
}
58 changes: 52 additions & 6 deletions lib/StaticAnalyzer/Core/MemRegion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ using namespace ento;
template<typename RegionTy> struct MemRegionManagerTrait;

template <typename RegionTy, typename A1>
RegionTy* MemRegionManager::getRegion(const A1 a1) {
RegionTy* MemRegionManager::getRegion(const A1 &a1) {
const typename MemRegionManagerTrait<RegionTy>::SuperRegionTy *superRegion =
MemRegionManagerTrait<RegionTy>::getSuperRegion(*this, a1);

Expand All @@ -54,7 +54,7 @@ RegionTy* MemRegionManager::getRegion(const A1 a1) {
}

template <typename RegionTy, typename A1>
RegionTy* MemRegionManager::getSubRegion(const A1 a1,
RegionTy *MemRegionManager::getSubRegion(const A1 &a1,
const MemRegion *superRegion) {
llvm::FoldingSetNodeID ID;
RegionTy::ProfileRegion(ID, a1, superRegion);
Expand All @@ -72,7 +72,7 @@ RegionTy* MemRegionManager::getSubRegion(const A1 a1,
}

template <typename RegionTy, typename A1, typename A2>
RegionTy* MemRegionManager::getRegion(const A1 a1, const A2 a2) {
RegionTy* MemRegionManager::getRegion(const A1 &a1, const A2 &a2) {
const typename MemRegionManagerTrait<RegionTy>::SuperRegionTy *superRegion =
MemRegionManagerTrait<RegionTy>::getSuperRegion(*this, a1, a2);

Expand All @@ -92,7 +92,7 @@ RegionTy* MemRegionManager::getRegion(const A1 a1, const A2 a2) {
}

template <typename RegionTy, typename A1, typename A2>
RegionTy* MemRegionManager::getSubRegion(const A1 a1, const A2 a2,
RegionTy *MemRegionManager::getSubRegion(const A1 &a1, const A2 &a2,
const MemRegion *superRegion) {
llvm::FoldingSetNodeID ID;
RegionTy::ProfileRegion(ID, a1, a2, superRegion);
Expand All @@ -110,7 +110,8 @@ RegionTy* MemRegionManager::getSubRegion(const A1 a1, const A2 a2,
}

template <typename RegionTy, typename A1, typename A2, typename A3>
RegionTy* MemRegionManager::getSubRegion(const A1 a1, const A2 a2, const A3 a3,
RegionTy *MemRegionManager::getSubRegion(const A1 &a1, const A2 &a2,
const A3 &a3,
const MemRegion *superRegion) {
llvm::FoldingSetNodeID ID;
RegionTy::ProfileRegion(ID, a1, a2, a3, superRegion);
Expand Down Expand Up @@ -255,6 +256,11 @@ void StaticGlobalSpaceRegion::Profile(llvm::FoldingSetNodeID &ID) const {
ID.AddPointer(getCodeRegion());
}

void GhostSpaceRegion::Profile(llvm::FoldingSetNodeID &ID) const {
ID.AddInteger(static_cast<unsigned>(getKind()));
ID.AddInteger(getTrait().getTraitID());
}

void StringRegion::ProfileRegion(llvm::FoldingSetNodeID& ID,
const StringLiteral* Str,
const MemRegion* superRegion) {
Expand Down Expand Up @@ -416,6 +422,17 @@ void CXXBaseObjectRegion::Profile(llvm::FoldingSetNodeID &ID) const {
ProfileRegion(ID, getDecl(), isVirtual(), superRegion);
}

void GhostSymbolicRegion::ProfileRegion(llvm::FoldingSetNodeID &ID,
SymbolRef Sym, const MemRegion *SReg) {
ID.AddInteger(GhostSymbolicRegionKind);
ID.AddPointer(Sym);
ID.AddPointer(SReg);
}

void GhostSymbolicRegion::Profile(llvm::FoldingSetNodeID &ID) const {
ProfileRegion(ID, Sym, getSuperRegion());
}

//===----------------------------------------------------------------------===//
// Region anchors.
//===----------------------------------------------------------------------===//
Expand All @@ -428,6 +445,7 @@ void StackArgumentsSpaceRegion::anchor() { }
void TypedRegion::anchor() { }
void TypedValueRegion::anchor() { }
void CodeTextRegion::anchor() { }
void GhostSpaceRegion::anchor() { }
void SubRegion::anchor() { }

//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -521,6 +539,10 @@ void VarRegion::dumpToStream(raw_ostream &os) const {
os << *cast<VarDecl>(D);
}

void GhostSymbolicRegion::dumpToStream(raw_ostream &os) const {
os << getTrait().getTraitDescription() << '{' << Sym << '}';
}

LLVM_DUMP_METHOD void RegionRawOffset::dump() const {
dumpToStream(llvm::errs());
}
Expand Down Expand Up @@ -565,6 +587,10 @@ void StackLocalsSpaceRegion::dumpToStream(raw_ostream &os) const {
os << "StackLocalsSpaceRegion";
}

void GhostSpaceRegion::dumpToStream(raw_ostream &os) const {
os << "GhostSpaceRegion{" << getTrait().getTraitDescription() << '}';
}

bool MemRegion::canPrintPretty() const {
return canPrintPrettyAsExpr();
}
Expand Down Expand Up @@ -647,7 +673,7 @@ const REG *MemRegionManager::LazyAllocate(REG*& region) {
}

template <typename REG, typename ARG>
const REG *MemRegionManager::LazyAllocate(REG*& region, ARG a) {
const REG *MemRegionManager::LazyAllocate(REG*& region, const ARG &a) {
if (!region) {
region = A.Allocate<REG>();
new (region) REG(this, a);
Expand Down Expand Up @@ -716,6 +742,18 @@ const CodeSpaceRegion *MemRegionManager::getCodeRegion() {
return LazyAllocate(code);
}

const GhostSpaceRegion *
MemRegionManager::getGhostSpaceRegion(const SmartStateTrait &Trait) {
GhostSpaceRegion *&R = GhostSpaceRegions[Trait.getTraitID()];

if (R)
return R;

R = A.Allocate<GhostSpaceRegion>();
new (R) GhostSpaceRegion(this, Trait);
return R;
}

//===----------------------------------------------------------------------===//
// Constructing regions.
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -882,6 +920,12 @@ MemRegionManager::getCXXStaticTempObjectRegion(const Expr *Ex) {
Ex, getGlobalsRegion(MemRegion::GlobalInternalSpaceRegionKind, nullptr));
}

const GhostSymbolicRegion *
MemRegionManager::getGhostSymbolicRegion(const SmartStateTrait &Trait,
SymbolRef Sym) {
return getSubRegion<GhostSymbolicRegion>(Sym, getGhostSpaceRegion(Trait));
}

const CompoundLiteralRegion*
MemRegionManager::getCompoundLiteralRegion(const CompoundLiteralExpr *CL,
const LocationContext *LC) {
Expand Down Expand Up @@ -1200,6 +1244,7 @@ RegionOffset MemRegion::getAsOffset() const {
case GlobalInternalSpaceRegionKind:
case GlobalSystemSpaceRegionKind:
case GlobalImmutableSpaceRegionKind:
case GhostSpaceRegionKind:
// Stores can bind directly to a region space to set a default value.
assert(Offset == 0 && !SymbolicOffsetBase);
goto Finish;
Expand All @@ -1221,6 +1266,7 @@ RegionOffset MemRegion::getAsOffset() const {
case ObjCStringRegionKind:
case VarRegionKind:
case CXXTempObjectRegionKind:
case GhostSymbolicRegionKind:
// Usual base regions.
goto Finish;

Expand Down
87 changes: 84 additions & 3 deletions lib/StaticAnalyzer/Core/RegionStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ class RegionStoreManager : public StoreManager {
bool scanReachableSymbols(Store S, const MemRegion *R,
ScanReachableSymbols &Callbacks) override;

bool hasAnyBinding(Store S, const MemRegion *R) override;

RegionBindingsRef removeSubRegionBindings(RegionBindingsConstRef B,
const SubRegion *R);

Expand Down Expand Up @@ -745,6 +747,67 @@ bool RegionStoreManager::scanReachableSymbols(Store S, const MemRegion *R,
return true;
}

bool RegionStoreManager::hasAnyBinding(Store S, const MemRegion *R) {
RegionBindingsRef B = getRegionBindings(S);

const MemRegion *BaseR = R->getBaseRegion();
const MemSpaceRegion *SpaceR = BaseR->getMemorySpace();

if (B.lookup(SpaceR))
return true; // The whole memory space has been wiped?

const ClusterBindings *Cluster = B.lookup(BaseR);
if (!Cluster) // No memory space wipes, no cluster bindings?
return false;
else if (BaseR == R) // Fast path for base regions.
return true;

RegionOffset RO = R->getAsOffset();
// If we are not certain of the offset, then any binding, which definitely
// exists in the cluster, may actually be inside R, so we stay careful.
// FIXME: Take constraints on symbols into account.
if (!RO.isValid() || RO.hasSymbolicOffset())
return true;

// The slowest path - for sub-regions.
int64_t Extent = -1; // "-1" represents symbolic extent.

// By now we are certain that R is not only a SubRegion,
// but also a non-base-region.
SVal ExtentVal = cast<SubRegion>(R)->getExtent(svalBuilder);
if (auto ConcreteExtentVal = ExtentVal.getAs<nonloc::ConcreteInt>())
Extent = ConcreteExtentVal->getValue().getSExtValue();

int64_t LeftEdge = RO.getOffset();
int64_t RightEdge = LeftEdge + Extent; // Not used in case of symbolic extent.

for (auto I = Cluster->begin(), E = Cluster->end(); I != E; ++I) {
const BindingKey &Key = I.getKey();

// FIXME: Take constraints on symbols into account.
if (Key.hasSymbolicOffset())
return true;

int64_t Offset = Key.getOffset();
if (Extent == -1) { // Symbolic offset?
if (Offset >= LeftEdge)
return true;
} else {
// FIXME: Essentially every binding is a segment, not a point.
// However, currently BindingKey does not store the size
// of this segment, only its left edge - the 'Offset'.
// Because this makes us skip cases of changed regions,
// which violates the contract of this method (say true if unsure),
// maybe throw away the whole loop and just return true
// whenever the cluster is non-empty?
if (Offset >= LeftEdge && Offset < RightEdge)
return true;
}
}
// All bindings in the cluster are outside R.
return false;
}

static inline bool isUnionField(const FieldRegion *FR) {
return FR->getDecl()->getParent()->isUnion();
}
Expand Down Expand Up @@ -2271,7 +2334,7 @@ RegionStoreManager::bindAggregate(RegionBindingsConstRef B,
namespace {
class removeDeadBindingsWorker :
public ClusterAnalysis<removeDeadBindingsWorker> {
SmallVector<const SymbolicRegion*, 12> Postponed;
SmallVector<const MemRegion*, 12> Postponed;
SymbolReaper &SymReaper;
const StackFrameContext *CurrentLCtx;

Expand Down Expand Up @@ -2321,6 +2384,15 @@ void removeDeadBindingsWorker::VisitAddedToCluster(const MemRegion *baseR,
return;
}

if (const GhostSymbolicRegion *GSR = dyn_cast<GhostSymbolicRegion>(baseR)) {
if (SymReaper.isLive(GSR->getSymbol()))
AddToWorkList(GSR, &C);
else
Postponed.push_back(GSR);

return;
}

if (isa<NonStaticGlobalSpaceRegion>(baseR)) {
AddToWorkList(baseR, &C);
return;
Expand All @@ -2346,6 +2418,8 @@ void removeDeadBindingsWorker::VisitCluster(const MemRegion *baseR,
// This means we should continue to track that symbol.
if (const SymbolicRegion *SymR = dyn_cast<SymbolicRegion>(baseR))
SymReaper.markLive(SymR->getSymbol());
if (const GhostSymbolicRegion *GSymR = dyn_cast<GhostSymbolicRegion>(baseR))
SymReaper.markLive(GSymR->getSymbol());

for (ClusterBindings::iterator I = C->begin(), E = C->end(); I != E; ++I) {
// Element index of a binding key is live.
Expand Down Expand Up @@ -2396,13 +2470,18 @@ bool removeDeadBindingsWorker::UpdatePostponed() {
// having done a scan.
bool changed = false;

for (SmallVectorImpl<const SymbolicRegion*>::iterator
for (SmallVectorImpl<const MemRegion*>::iterator
I = Postponed.begin(), E = Postponed.end() ; I != E ; ++I) {
if (const SymbolicRegion *SR = *I) {
if (const auto *SR = dyn_cast_or_null<SymbolicRegion>(*I)) {
if (SymReaper.isLive(SR->getSymbol())) {
changed |= AddToWorkList(SR);
*I = nullptr;
}
} else if (const auto *GSR = dyn_cast_or_null<GhostSymbolicRegion>(*I)) {
if (SymReaper.isLive(GSR->getSymbol())) {
changed |= AddToWorkList(GSR);
*I = nullptr;
}
}
}

Expand Down Expand Up @@ -2439,6 +2518,8 @@ StoreRef RegionStoreManager::removeDeadBindings(Store store,

if (const SymbolicRegion *SymR = dyn_cast<SymbolicRegion>(Base))
SymReaper.maybeDead(SymR->getSymbol());
if (const GhostSymbolicRegion *GSymR = dyn_cast<GhostSymbolicRegion>(Base))
SymReaper.maybeDead(GSymR->getSymbol());

// Mark all non-live symbols that this binding references as dead.
const ClusterBindings &Cluster = I.getData();
Expand Down
4 changes: 3 additions & 1 deletion lib/StaticAnalyzer/Core/Store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ const MemRegion *StoreManager::castRegion(const MemRegion *R, QualType CastToTy)
case MemRegion::StaticGlobalSpaceRegionKind:
case MemRegion::GlobalInternalSpaceRegionKind:
case MemRegion::GlobalSystemSpaceRegionKind:
case MemRegion::GlobalImmutableSpaceRegionKind: {
case MemRegion::GlobalImmutableSpaceRegionKind:
case MemRegion::GhostSpaceRegionKind: {
llvm_unreachable("Invalid region cast");
}

Expand All @@ -126,6 +127,7 @@ const MemRegion *StoreManager::castRegion(const MemRegion *R, QualType CastToTy)
case MemRegion::VarRegionKind:
case MemRegion::CXXTempObjectRegionKind:
case MemRegion::CXXBaseObjectRegionKind:
case MemRegion::GhostSymbolicRegionKind:
return MakeElementRegion(R, PointeeTy);

case MemRegion::ElementRegionKind: {
Expand Down
3 changes: 3 additions & 0 deletions lib/StaticAnalyzer/Core/SymbolManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,9 @@ bool SymbolReaper::isLiveRegion(const MemRegion *MR) {
if (const SymbolicRegion *SR = dyn_cast<SymbolicRegion>(MR))
return isLive(SR->getSymbol());

if (const GhostSymbolicRegion *GSR = dyn_cast<GhostSymbolicRegion>(MR))
return isLive(GSR->getSymbol());

if (const VarRegion *VR = dyn_cast<VarRegion>(MR))
return isLive(VR, true);

Expand Down
10 changes: 9 additions & 1 deletion test/Analysis/simple-stream-checks.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// RUN: %clang_cc1 -analyze -analyzer-checker=core,alpha.unix.SimpleStream -verify %s
// RUN: %clang_cc1 -analyze -analyzer-checker=core,alpha.unix.SimpleStream -DFIXIT_V1 -verify %s
// RUN: %clang_cc1 -analyze -analyzer-checker=core,alpha.unix.SimpleStreamV2 -DFIXIT_V2 -verify %s

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

Expand Down Expand Up @@ -89,3 +90,10 @@ void testPassToSystemHeaderFunctionIndirectly() {
fs.p = fopen("myfile.txt", "w");
fakeSystemHeaderCall(&fs); // invalidates fs, making fs.p unreachable
} // no-warning

#ifndef FIXIT_V1
void testOverwrite() {
FILE *F = fopen("myfile.txt", "w");
F = 0;
} // expected-warning{{Opened file is never closed; potential resource leak}}
#endif