Skip to content

Commit a350346

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 8bc093c commit a350346

5 files changed

Lines changed: 101 additions & 0 deletions

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 CheckIO::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
@@ -64,6 +64,9 @@ class CPPCHECKLIB CheckIO : public Check {
6464
/** @brief scanf can crash if width specifiers are not used */
6565
void invalidScanf();
6666

67+
/** @brief %Check wrong usage of feof */
68+
void checkWrongfeofUsage();
69+
6770
/** @brief %Checks type and number of arguments given to functions like printf or scanf*/
6871
void checkWrongPrintfScanfArguments();
6972

@@ -109,6 +112,7 @@ class CPPCHECKLIB CheckIO : public Check {
109112
void seekOnAppendedFileError(const Token *tok);
110113
void incompatibleFileOpenError(const Token *tok, const std::string &filename);
111114
void invalidScanfError(const Token *tok);
115+
void wrongfeofUsage(const Token *tok);
112116
void wrongPrintfScanfArgumentsError(const Token* tok,
113117
const std::string &functionName,
114118
nonneg int numFormat,

releasenotes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ New checks:
99
- funcArgNamesDifferentUnnamed warns on function declarations/definitions where a parameter in either location is unnamed
1010
- uninitMemberVarNoCtor warns on user-defined types where some but not all members requiring initialization have in-class initializers.
1111
- fcloseInLoopCondition warns when fclose() is used as a while loop condition, which may skip the loop body or double-close the file handle.
12+
- Warn when feof() is used as a while loop condition (wrongfeofUsage).
1213

1314
C/C++ support:
1415
-

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)