diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td --- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td +++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td @@ -516,6 +516,10 @@ HelpText<"Check improper use of chroot">, Documentation; +def ErrorReturnChecker : Checker<"ErrorReturn">, + HelpText<"Check for unchecked error return values">, + Documentation; + def PthreadLockChecker : Checker<"PthreadLock">, HelpText<"Simple lock -> unlock checker">, Dependencies<[PthreadLockBase]>, diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt --- a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt +++ b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt @@ -40,6 +40,7 @@ DynamicTypePropagation.cpp DynamicTypeChecker.cpp EnumCastOutOfRangeChecker.cpp + ErrorReturnChecker.cpp ExprInspectionChecker.cpp FixedAddressChecker.cpp FuchsiaHandleChecker.cpp diff --git a/clang/lib/StaticAnalyzer/Checkers/ErrorReturnChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/ErrorReturnChecker.cpp new file mode 100644 --- /dev/null +++ b/clang/lib/StaticAnalyzer/Checkers/ErrorReturnChecker.cpp @@ -0,0 +1,510 @@ +//===-- ErrorReturnChecker.cpp ------------------------------------*- C++ -*--// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines ErrorReturnChecker, a builtin checker that checks for +// error checking of certain C API function return values. +// This check is taken from SEI CERT ERR33-C: +// https://wiki.sei.cmu.edu/confluence/display/c/ERR33-C.+Detect+and+handle+standard+library+errors +// +// About the checker: +// It involves a predefined set of system call functions that can fail and +// return a specific error code on failure. (This list is provided by the CERT +// rule.) The checker tries to verify if there is a statement in the code that +// checks the returned value. For different kinds of error return values +// different kinds of check statements are accepted. The first use (that is not +// assignment or pass to function) of the return value of the function call that +// is checked should be the check statement. Any other use of the returned value +// (except assignment or pass to function call) is taken as use before check and +// reported as checker warning. Additionally, if the return value is not used at +// all a warning is generated for unchecked return value. Passing the return +// value to a system function results in warning too. +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/Expr.h" +#include "clang/AST/ParentMap.h" +#include "clang/AST/Stmt.h" +#include "clang/AST/StmtVisitor.h" +#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/CheckerManager.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h" +#include + +using namespace clang; +using namespace ento; + +namespace { + +/// Interface and abstraction for various kinds of error return value and way of +/// checking it. The "test" functions are called if a corresponding construct in +/// the code is found. The functions should return true the code is found to be +/// acceptable as error check. +class ErrorReturnCheckKind { +public: + /// Test if an encountered binary operator where the return value is involved + /// is a valid check statement. The return value appears in one side of the + /// operator (`ChildIsLHS` indicates if it is on the LHS). If the other side + /// contains a known (mostly constant) value, it is already calculated in + /// `KnownValue`. `RetTy` is the type of the return value (return type of the + /// function call in the code to check). + virtual bool testBinOpForCheckStatement(BasicValueFactory &BVF, + const BinaryOperator *BinOp, + const llvm::APSInt *KnownValue, + QualType RetTy, + bool ChildIsLHS) const = 0; +}; + +/// Error return is a -1 or any negative value (both is accepted). +/// More precise, the check for error return value should be comparison to -1 +/// or relational comparison to 0. +/// This is to be used with signed types only. +class EOFOrNegativeErrorReturn : public ErrorReturnCheckKind { +public: + bool testBinOpForCheckStatement(BasicValueFactory &BVF, + const BinaryOperator *BinOp, + const llvm::APSInt *KnownValue, + QualType RetTy, + bool ChildIsLHS) const override { + if (!KnownValue) + return false; + + bool KnownNull = KnownValue->isNullValue(); + bool KnownEOF = ((*KnownValue) == BVF.getValue(-1, RetTy)); + + if (ChildIsLHS) { + switch (BinOp->getOpcode()) { + case BO_EQ: // 'X == -1' + case BO_NE: // 'X != -1' + return KnownEOF; + case BO_LT: // 'X < 0' + return KnownNull; + case BO_GE: // 'X >= 0' + return KnownNull; + case BO_LE: // 'X <= -1' + return KnownEOF; + case BO_GT: // 'X > -1' + return KnownEOF; + default: + return false; + } + } else { + switch (BinOp->getOpcode()) { + case BO_EQ: // '-1 == X' + case BO_NE: // '-1 != X' + return KnownEOF; + case BO_GT: // '0 > X' + return KnownNull; + case BO_LE: // '0 <= X' + return KnownNull; + case BO_GE: // '-1 >= X' + return KnownEOF; + case BO_LT: // '-1 < X' + return KnownEOF; + default: + return false; + } + } + return false; + } +}; + +/// Description of an API function to check. +struct FnInfo { + /// Error return check kind for the function. + ErrorReturnCheckKind *ErrorReturnKind; + + /// Is an unchecked use of return value error. + bool UncheckedUseIsError; + + /// Return type of the function (initialized at runtime). + mutable QualType RetTy; + + FnInfo(ErrorReturnCheckKind *ErrorReturnKind) + : ErrorReturnKind(ErrorReturnKind) { + ; + } +}; + +/// Information about a specific function call that has an error return code to +/// check. This data is stored in a map and indexed by the SymbolRef that stands +/// for the result of the function call. +struct CalledFunctionData { + /// Point out the kind of the function that was called. + const FnInfo *Info; + /// Source range of the calling statement. + SourceRange CallLocation; + + CalledFunctionData(const CalledFunctionData &CFD) + : Info(CFD.Info), CallLocation(CFD.CallLocation) {} + CalledFunctionData(const FnInfo *Info, const SourceRange &CallLocation) + : Info{Info}, CallLocation{CallLocation} {} + + CalledFunctionData &operator=(const CalledFunctionData &CFD) { + Info = CFD.Info; + CallLocation = CFD.CallLocation; + return *this; + } + + void Profile(llvm::FoldingSetNodeID &ID) const { + ID.AddPointer(Info); + ID.AddInteger(CallLocation.getBegin().getRawEncoding()); + } + + bool operator==(const CalledFunctionData &CFD) const { + return Info == CFD.Info && CallLocation == CFD.CallLocation; + } +}; + +class ErrorReturnChecker + : public Checker { + mutable std::unique_ptr BT_UncheckedUse; + + void checkAccess(CheckerContext &C, ProgramStateRef State, const Stmt *LoadS, + SymbolRef CallSym, const CalledFunctionData *CFD) const; + ProgramStateRef processEscapedParams(CheckerContext &C, const CallEvent &Call, + ProgramStateRef State) const; + const FnInfo *findFunctionToCheck(CheckerContext &C, + const CallEvent &Call) const; + +public: + void checkPostCall(const CallEvent &Call, CheckerContext &C) const; + void checkLocation(SVal L, bool IsLoad, const Stmt *S, + CheckerContext &C) const; + void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const; + ProgramStateRef checkPointerEscape(ProgramStateRef State, + const InvalidatedSymbols &Escaped, + const CallEvent *Call, + PointerEscapeKind Kind) const; + +private: + EOFOrNegativeErrorReturn CheckForEOFOrNegative; + + CallDescriptionMap CheckedFunctions = { + {{"fputs", 2}, FnInfo{&CheckForEOFOrNegative}}, + {{"fputws", 2}, FnInfo{&CheckForEOFOrNegative}}, + }; +}; + +/// Result of the ErrorCheckTestStmtVisitor. +enum VisitResult { + // Value use found that is not an error check. + NoCheckUseFound, + // An error check was found. + CheckFound, + // Assignment (like) condition was found (something that returns the same + // value as `Child`), the check should continue on upper level of the + // expression (with the current parent as new child). + ContinueAtParentStmt, + // Value appeared in any other way. + StopExamineNoError +}; + +/// Determine if a statement is an error-check for a return value. +/// The statement to check is stored in `Child`. +/// This can be the actual load statement that uses the return value, +/// or a parent statement of it that stands for the same value. +/// See `ErrorReturnChecker::checkAccess` for the calling algorithm. +class ErrorCheckTestStmtVisitor + : public ConstStmtVisitor { + CheckerContext &C; + /// Currently examined child statement. + const Stmt *Child; + /// Data about function whose return value is checked. + const CalledFunctionData *CFD; + const ParentMap &PM; + + /// Check if `Child` is a child of `Parent` in `ParentMap`. + bool isChildOf(const Stmt *Parent) const { + for (const Stmt *C = Child; C; C = PM.getParent(C)) + if (C == Parent) + return true; + return false; + }; + + /// Determine the "known value" (if a constant) of an Expr. + const llvm::APSInt *getKnownConstantVal(const Expr *E) const { + Optional ConstantVal = C.getSValBuilder().getConstantVal(E); + if (ConstantVal) + return C.getSValBuilder().getKnownValue(C.getState(), *ConstantVal); + return nullptr; + } + +public: + ErrorCheckTestStmtVisitor(CheckerContext &C, const Stmt *Child, + const CalledFunctionData *CFD) + : C(C), Child(Child), CFD(CFD), + PM(C.getLocationContext()->getParentMap()) {} + + VisitResult VisitBinaryOperator(const BinaryOperator *BO) { + // Try to find the child expression on one side of the operator. + Expr *OtherS = nullptr; + for (const Stmt *P = Child; P && !OtherS; P = PM.getParent(P)) { + if (P == BO->getLHS()) + OtherS = BO->getRHS(); + else if (P == BO->getRHS()) + OtherS = BO->getLHS(); + } + assert(OtherS && "Invalid parent at binary operator."); + + BinaryOperatorKind Op = BO->getOpcode(); + if (Op == BO_Assign) { + // Value of child is transferred to parent, can continue with parent. + assert(OtherS == BO->getLHS() && "Loaded value not on assignment RHS."); + return ContinueAtParentStmt; + } + + // `Child` appears in the binary operator. + // Perform a specific check on the binary operator to determine if it is + // an error-check statement. + if (CFD->Info->ErrorReturnKind->testBinOpForCheckStatement( + C.getSValBuilder().getBasicValueFactory(), BO, + getKnownConstantVal(OtherS), CFD->Info->RetTy, + OtherS == BO->getRHS())) + return CheckFound; + // Value appears in binary operator in other way than error check. + return NoCheckUseFound; + } + + VisitResult VisitCallExpr(const CallExpr *CE) { + const FunctionDecl *CalledF = C.getCalleeDecl(CE); + SourceLocation Loc = CalledF->getLocation(); + // Check if system function is called. + // It is assumed that any system function does not accept value that is + // an error return value from any other function. + // In other words error check is required before system function is called + // with the value. + if (Loc.isValid() && C.getSourceManager().isInSystemHeader(Loc)) + return NoCheckUseFound; + // Non-system function call is no error, the call may be inlined and can + // contain error check. + return StopExamineNoError; + } + + // Value appears in other expression. + VisitResult VisitExpr(const Stmt *S) { return NoCheckUseFound; } + + VisitResult VisitDeclStmt(const DeclStmt *Decl) { + // Value is used at initialization. + return StopExamineNoError; + } + + VisitResult VisitIfStmt(const IfStmt *S) { + if (isChildOf(S->getCond())) + return NoCheckUseFound; + return StopExamineNoError; + } + + VisitResult VisitForStmt(const ForStmt *S) { + if (isChildOf(S->getInit()) || isChildOf(S->getCond()) || + isChildOf(S->getInc())) + return NoCheckUseFound; + return StopExamineNoError; + } + + VisitResult VisitDoStmt(const DoStmt *S) { + if (isChildOf(S->getCond())) + return NoCheckUseFound; + return StopExamineNoError; + } + + VisitResult VisitWhileStmt(const WhileStmt *S) { + if (isChildOf(S->getCond())) + return NoCheckUseFound; + return StopExamineNoError; + } + + // Value appears in other statement. + // FIXME: Statements that affects control-flow should be checked separately. + // For example `Child` may appear as a condition of `if`. + VisitResult VisitStmt(const Stmt *S) { return StopExamineNoError; } +}; + +} // end anonymous namespace + +REGISTER_MAP_WITH_PROGRAMSTATE(CalledFunctionDataMap, SymbolRef, + CalledFunctionData) + +void ErrorReturnChecker::checkAccess(CheckerContext &C, ProgramStateRef State, + const Stmt *LoadS, SymbolRef CallSym, + const CalledFunctionData *CFD) const { + const ParentMap &PM = C.getLocationContext()->getParentMap(); + //llvm::errs()<<"LoadS\n"; + //LoadS->dumpColor(); + + while (LoadS) { + const Stmt *ParentS = PM.getParentIgnoreParenCasts(LoadS); + //llvm::errs()<<"ParentS\n"; + //ParentS->dumpColor(); + + ErrorCheckTestStmtVisitor FindErrorCheck{C, LoadS, CFD}; + switch (FindErrorCheck.Visit(ParentS)) { + case NoCheckUseFound: { + if (!BT_UncheckedUse) + BT_UncheckedUse.reset( + new BuiltinBug(this, "Use of unchecked return value", + "Use of return value that was not checked for error")); + + SourceRange CallLocation = CFD->CallLocation; + State = State->remove(CallSym); + + ExplodedNode *N = C.generateNonFatalErrorNode(State); + if (!N) { + C.addTransition(State); + return; + } + + auto Report = std::make_unique( + *BT_UncheckedUse, BT_UncheckedUse->getDescription(), N); + // Report->markInteresting(CallSym); + Report->addRange(CallLocation); + C.emitReport(std::move(Report)); + + //auto Report = std::make_unique(*BT_UncheckedUse, BT_UncheckedUse->getDescription(), PathDiagnosticLocation{LoadS, C.getSourceManager(), C.getLocationContext()}); + //Report->addRange(CallLocation); + //C.emitReport(std::move(Report)); + + return; + } + case CheckFound: + // A correct error check was found, remove from state. + State = State->remove(CallSym); + C.addTransition(State); + return; + + case ContinueAtParentStmt: + // Continue checking at upper level (check with result of assignment). + LoadS = ParentS; + continue; + + case StopExamineNoError: + C.addTransition(State); + return; + }; + } +} + +ProgramStateRef ErrorReturnChecker::processEscapedParams( + CheckerContext &C, const CallEvent &Call, ProgramStateRef State) const { + for (unsigned int I = 0, E = Call.getNumArgs(); I < E; ++I) { + SVal V = Call.getArgSVal(I); + SymbolRef Sym = V.getAsSymbol(); + if (Sym) { + State = State->remove(Sym); + } + } + return State; +} + +const FnInfo * +ErrorReturnChecker::findFunctionToCheck(CheckerContext &C, + const CallEvent &Call) const { + const auto *FD = dyn_cast_or_null(Call.getDecl()); + const ParentMap &PM = C.getLocationContext()->getParentMap(); + + if (!FD || FD->getKind() != Decl::Function) + return nullptr; + + if (!Call.isGlobalCFunction() || !Call.isInSystemHeader()) + return nullptr; + + const FnInfo *Fn = CheckedFunctions.lookup(Call); + if (!Fn) + return nullptr; + + const Stmt *S = PM.getParent(Call.getOriginExpr()); + + // Check for explicit cast to void. + if (auto *Cast = dyn_cast(S)) { + if (Cast->getTypeAsWritten().getTypePtr()->isVoidType()) + return nullptr; + } + + // The call should have a symbolic return value to analyze it. + SVal RetSV = Call.getReturnValue(); + if (RetSV.isUnknownOrUndef()) + return nullptr; + SymbolRef RetSym = RetSV.getAsSymbol(); + if (!RetSym) + return nullptr; + + // Lazy-init the return type when the function is found. + if (Fn->RetTy.isNull()) + Fn->RetTy = FD->getReturnType(); + + return Fn; +} + +void ErrorReturnChecker::checkPostCall(const CallEvent &Call, + CheckerContext &C) const { + ProgramStateRef State = C.getState(); + + State = processEscapedParams(C, Call, State); + + const FnInfo *Fn = findFunctionToCheck(C, Call); + if (!Fn) { + C.addTransition(State); + return; + } + + SymbolRef RetSym = Call.getReturnValue().getAsSymbol(); + + CalledFunctionData CFD{Fn, Call.getSourceRange()}; + State = State->set(RetSym, CFD); + + checkAccess(C, State, Call.getOriginExpr(), RetSym, &CFD); +} + +void ErrorReturnChecker::checkLocation(SVal L, bool IsLoad, const Stmt *S, + CheckerContext &C) const { + if (!IsLoad) + return; + if (L.isUnknownOrUndef()) + return; + + auto Location = L.castAs().getAs(); + if (!Location) + return; + + ProgramStateRef State = C.getState(); + SymbolRef Sym = State->getSVal(*Location).getAsSymbol(); + if (!Sym) + return; + + const CalledFunctionData *CFD = State->get(Sym); + if (!CFD) + return; + + checkAccess(C, State, S, Sym, CFD); +} + +ProgramStateRef ErrorReturnChecker::checkPointerEscape( + ProgramStateRef State, const InvalidatedSymbols &Escaped, + const CallEvent *Call, PointerEscapeKind Kind) const { + for (InvalidatedSymbols::const_iterator I = Escaped.begin(), + E = Escaped.end(); + I != E; ++I) { + SymbolRef Sym = *I; + State = State->remove(Sym); + } + return State; +} + +void ento::registerErrorReturnChecker(CheckerManager &Mgr) { + Mgr.registerChecker(); +} + +bool ento::shouldRegisterErrorReturnChecker(const CheckerManager &Mgr) { + return true; +} diff --git a/clang/test/Analysis/Inputs/system-header-simulator.h b/clang/test/Analysis/Inputs/system-header-simulator.h --- a/clang/test/Analysis/Inputs/system-header-simulator.h +++ b/clang/test/Analysis/Inputs/system-header-simulator.h @@ -49,6 +49,7 @@ size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); int fputc(int ch, FILE *stream); +int fputs(const char *restrict s, FILE *restrict stream); int fseek(FILE *__stream, long int __off, int __whence); long int ftell(FILE *__stream); void rewind(FILE *__stream); @@ -100,6 +101,7 @@ //The following are fake system header functions for generic testing. void fakeSystemHeaderCallInt(int *); void fakeSystemHeaderCallIntPtr(int **); +void fakeSystemHeaderCallIntVal(int); // Some data strauctures may hold onto the pointer and free it later. void fake_insque(void *, void *); diff --git a/clang/test/Analysis/error-return.c b/clang/test/Analysis/error-return.c new file mode 100644 --- /dev/null +++ b/clang/test/Analysis/error-return.c @@ -0,0 +1,181 @@ +// RUN: %clang_cc1 -analyze -analyzer-checker=alpha.unix.ErrorReturn -verify %s + +#include "Inputs/system-header-simulator.h" + +FILE *file(); + +void test_EOFOrNeg_LT_Good() { + if (fputs("str", file()) < 0) { + } +} + +void test_EOFOrNeg_LT_Bad() { + if (fputs("str", file()) < -1) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_GT_Good() { + if (fputs("str", file()) > -1) { + } +} + +void test_EOFOrNeg_GT_Bad() { + if (fputs("str", file()) > 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_LE_Good() { + if (fputs("str", file()) <= -1) { + } +} + +void test_EOFOrNeg_LE_Bad() { + if (fputs("str", file()) <= 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_GE_Good() { + if (fputs("str", file()) >= 0) { + } +} + +void test_EOFOrNeg_GE_Bad() { + if (fputs("str", file()) >= -1) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_EQ_Good() { + if (fputs("str", file()) == -1) { + } +} + +void test_EOFOrNeg_EQ_Bad() { + if (fputs("str", file()) == 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_NE_Good() { + if (fputs("str", file()) != -1) { + } +} + +void test_EOFOrNeg_NE_Bad() { + if (fputs("str", file()) != 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_EQ_BadVal() { + if (fputs("str", file()) == -2) { // expected-warning{{Use of return value that was not checked}} + } + if (fputs("str", file()) == 1) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_VarAssign() { + int X = fputs("str", file()); + if (X != 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_VarAssignInCond() { + int X; + if ((X = fputs("str", file())) != 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_VarAssign1() { + int X = fputs("str", file()); + int Y = X; + if (Y != 0) { // expected-warning{{Use of return value that was not checked}} + } +} + +void badcheck(int X) { + if (X == 0) { } // expected-warning{{Use of return value that was not checked}} +} + +void test_EOFOrNeg_Call() { + int X = fputs("str", file()); + badcheck(X); +} + +void test_EOFOrNeg_Syscall() { + int X = fputs("str", file()); + fakeSystemHeaderCallIntVal(X); // expected-warning{{Use of return value that was not checked}} + fakeSystemHeaderCallIntVal(fputs("str", file())); // expected-warning{{Use of return value that was not checked}} +} + +void test_EOFOrNeg_Use_LNot() { + int X = fputs("str", file()); + if (!X) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_Use_Add() { + int X = fputs("str", file()); + int Y = X + 1; // expected-warning{{Use of return value that was not checked}} +} + +void test_EOFOrNeg_If() { + int X = fputs("str", file()); + if (X) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_IfCond() { + if (fputs("str", file())) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_ForInit() { + for (fputs("str", file());;) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_ForCond() { + for (; fputs("str", file());) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_ForInc() { + for (;; fputs("str", file())) { // expected-warning{{Use of return value that was not checked}} + } +} + +void test_EOFOrNeg_DoCond() { + do { + } while (fputs("str", file())); // expected-warning{{Use of return value that was not checked}} +} + +void test_EOFOrNeg_WhileCond() { + while (fputs("str", file())) { + }; // expected-warning{{Use of return value that was not checked}} +} + +void unknown1(int); + +void test_EOFOrNeg_EscapeCall() { + int X = fputs("str", file()); + unknown1(X); + int Y = X + 1; +} + +int GlobalInt; + +void test_EOFOrNeg_EscapeGlobalAssign() { + GlobalInt = fputs("str", file()); + int X = GlobalInt + 1; +} + +void test_EOFOrNeg_NoErrorAfterGoodCheck() { + int X = fputs("str", file()); + if (X < 0) { + } + if (X < 1) { + } +} + +void test_EOFOrNeg_Unused() { + // FIXME: Detect this + fputs("str", file()); +}