Skip to content

Commit 1962f91

Browse files
Fix #958: warn when feof() is used as a while loop condition
feof() only returns true after a read has already failed, causing the loop body to execute once more after the last successful read. Read errors also go undetected since feof() does not distinguish them from EOF. Signed-off-by: Francois Berder <fberder@outlook.fr>
1 parent 0491f59 commit 1962f91

5 files changed

Lines changed: 101 additions & 1 deletion

File tree

lib/checkers.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ namespace checkers {
101101
{"CheckFunctions::useStandardLibrary","style"},
102102
{"CheckIO::checkCoutCerrMisusage","c"},
103103
{"CheckIO::checkFileUsage",""},
104+
{"CheckIO::checkWrongfeofUsage",""},
104105
{"CheckIO::checkWrongPrintfScanfArguments",""},
105106
{"CheckIO::invalidScanf",""},
106107
{"CheckLeakAutoVar::check","notclang"},

lib/checkio.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,68 @@ void CheckIOImpl::invalidScanfError(const Token *tok)
507507
CWE119, Certainty::normal);
508508
}
509509

510+
static const Token* findFileReadCall(const Token *start, const Token *end, int varid)
511+
{
512+
const Token* found = Token::findmatch(start, "fgets|fgetc|getc|fread|fscanf (", end);
513+
while (found) {
514+
const std::vector<const Token*> args = getArguments(found);
515+
if (!args.empty()) {
516+
const bool match = (found->str() == "fscanf")
517+
? args.front()->varId() == varid
518+
: args.back()->varId() == varid;
519+
if (match)
520+
return found;
521+
}
522+
found = Token::findmatch(found->next(), "fgets|fgetc|getc|fread|fscanf (", end);
523+
}
524+
return nullptr;
525+
}
526+
527+
void CheckIO::checkWrongfeofUsage()
528+
{
529+
const SymbolDatabase *symbolDatabase = mTokenizer->getSymbolDatabase();
530+
531+
logChecker("CheckIO::checkWrongfeofUsage");
532+
533+
for (const Scope * scope : symbolDatabase->functionScopes) {
534+
for (const Token *tok = scope->bodyStart->next(); tok != scope->bodyEnd; tok = tok->next()) {
535+
if (!Token::simpleMatch(tok, "while ( ! feof ("))
536+
continue;
537+
538+
// Bail out if we cannot identify file pointer
539+
const int fpVarId = tok->tokAt(5)->varId();
540+
if (fpVarId == 0)
541+
continue;
542+
543+
// Usage of feof is correct if a read happens before and within the loop.
544+
// However, if we find a control flow statement in between the fileReadCall
545+
// token and the while loop condition, then we bail out.
546+
const Token *endCond = tok->linkAt(1);
547+
const Token *endBody = endCond->linkAt(1);
548+
549+
const Token *prevFileReadCallTok = findFileReadCall(scope->bodyStart, tok, fpVarId);
550+
const Token *loopFileReadCallTok = findFileReadCall(tok, endBody, fpVarId);
551+
const Token *prevControlFlowTok = Token::findmatch(prevFileReadCallTok, "return|break|goto|continue|throw", tok);
552+
const Token *loopControlFlowTok = Token::findmatch(tok, "return|break|goto|continue|throw", loopFileReadCallTok);
553+
554+
if (prevFileReadCallTok && loopFileReadCallTok && !prevControlFlowTok && !loopControlFlowTok)
555+
continue;
556+
557+
wrongfeofUsage(getCondTok(tok));
558+
}
559+
}
560+
}
561+
562+
void CheckIO::wrongfeofUsage(const Token * tok)
563+
{
564+
reportError(tok, Severity::warning,
565+
"wrongfeofUsage",
566+
"Using feof() as a loop condition causes the last line to be processed twice.\n"
567+
"feof() returns true only after a read has failed due to end-of-file, so the loop "
568+
"body executes once more after the last successful read. Check the return value of "
569+
"the read function instead (e.g. fgets, fread, fscanf).");
570+
}
571+
510572
//---------------------------------------------------------------------------
511573
// printf("%u", "xyz"); // Wrong argument type
512574
// printf("%u%s", 1); // Too few arguments
@@ -2057,6 +2119,7 @@ void CheckIO::runChecks(const Tokenizer &tokenizer, ErrorLogger& errorLogger)
20572119
checkIO.checkWrongPrintfScanfArguments();
20582120
checkIO.checkCoutCerrMisusage();
20592121
checkIO.checkFileUsage();
2122+
checkIO.checkWrongfeofUsage();
20602123
checkIO.invalidScanf();
20612124
}
20622125

@@ -2072,6 +2135,7 @@ void CheckIO::getErrorMessages(ErrorLogger& errorLogger, const Settings &setting
20722135
c.fcloseInLoopConditionError(nullptr, "fp");
20732136
c.seekOnAppendedFileError(nullptr);
20742137
c.incompatibleFileOpenError(nullptr, "tmp");
2138+
c.wrongfeofUsage(nullptr);
20752139
c.invalidScanfError(nullptr);
20762140
c.wrongPrintfScanfArgumentsError(nullptr, "printf",3,2);
20772141
c.invalidScanfArgTypeError_s(nullptr, 1, "s", nullptr);

lib/checkio.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class CPPCHECKLIB CheckIOImpl : public CheckImpl {
8484
/** @brief scanf can crash if width specifiers are not used */
8585
void invalidScanf();
8686

87+
/** @brief %Check wrong usage of feof */
88+
void checkWrongfeofUsage();
89+
8790
/** @brief %Checks type and number of arguments given to functions like printf or scanf*/
8891
void checkWrongPrintfScanfArguments();
8992

@@ -129,6 +132,7 @@ class CPPCHECKLIB CheckIOImpl : public CheckImpl {
129132
void seekOnAppendedFileError(const Token *tok);
130133
void incompatibleFileOpenError(const Token *tok, const std::string &filename);
131134
void invalidScanfError(const Token *tok);
135+
void wrongfeofUsage(const Token *tok);
132136
void wrongPrintfScanfArgumentsError(const Token* tok,
133137
const std::string &functionName,
134138
nonneg int numFormat,

releasenotes.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Major bug fixes & crashes:
55
-
66

77
New checks:
8-
-
8+
- Warn when feof() is used as a while loop condition (wrongfeofUsage).
99

1010
C/C++ support:
1111
-

test/testio.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class TestIO : public TestFixture {
4545
TEST_CASE(seekOnAppendedFile);
4646
TEST_CASE(fflushOnInputStream);
4747
TEST_CASE(incompatibleFileOpen);
48+
TEST_CASE(testWrongfeofUsage); // #958
4849

4950
TEST_CASE(testScanf1); // Scanf without field limiters
5051
TEST_CASE(testScanf2);
@@ -766,6 +767,36 @@ class TestIO : public TestFixture {
766767
ASSERT_EQUALS("[test.cpp:3:16]: (warning) The file '\"tmp\"' is opened for read and write access at the same time on different streams [incompatibleFileOpen]\n", errout_str());
767768
}
768769

770+
void testWrongfeofUsage() { // ticket #958
771+
check("void foo(FILE * fp) {\n"
772+
" while (!feof(fp)) \n"
773+
" {\n"
774+
" char line[100];\n"
775+
" fgets(line, sizeof(line), fp);\n"
776+
" }\n"
777+
"}");
778+
ASSERT_EQUALS("[test.cpp:2:10]: (warning) Using feof() as a loop condition causes the last line to be processed twice. [wrongfeofUsage]\n", errout_str());
779+
780+
check("int foo(FILE *fp) {\n"
781+
" char line[100];\n"
782+
" while (fgets(line, sizeof(line), fp)) {}\n"
783+
" if (!feof(fp))\n"
784+
" return 1;\n"
785+
" return 0;\n"
786+
"}");
787+
ASSERT_EQUALS("", errout_str());
788+
789+
check("void foo(FILE *fp){\n"
790+
" char line[100];\n"
791+
" fgets(line, sizeof(line), fp);\n"
792+
" while (!feof(fp)){\n"
793+
" dostuff(line);\n"
794+
" fgets(line, sizeof(line), fp);"
795+
" }\n"
796+
"}");
797+
ASSERT_EQUALS("", errout_str());
798+
}
799+
769800

770801
void testScanf1() {
771802
check("void foo() {\n"

0 commit comments

Comments
 (0)