Skip to content

Commit 905393d

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 89da2d1 commit 905393d

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
@@ -481,6 +481,68 @@ void CheckIO::invalidScanfError(const Token *tok)
481481
CWE119, Certainty::normal);
482482
}
483483

484+
static const Token* findFileReadCall(const Token *start, const Token *end, int varid)
485+
{
486+
const Token* found = Token::findmatch(start, "fgets|fgetc|getc|fread|fscanf (", end);
487+
while (found) {
488+
const std::vector<const Token*> args = getArguments(found);
489+
if (!args.empty()) {
490+
const bool match = (found->str() == "fscanf")
491+
? args.front()->varId() == varid
492+
: args.back()->varId() == varid;
493+
if (match)
494+
return found;
495+
}
496+
found = Token::findmatch(found->next(), "fgets|fgetc|getc|fread|fscanf (", end);
497+
}
498+
return nullptr;
499+
}
500+
501+
void CheckIO::checkWrongfeofUsage()
502+
{
503+
const SymbolDatabase *symbolDatabase = mTokenizer->getSymbolDatabase();
504+
505+
logChecker("CheckIO::checkWrongfeofUsage");
506+
507+
for (const Scope * scope : symbolDatabase->functionScopes) {
508+
for (const Token *tok = scope->bodyStart->next(); tok != scope->bodyEnd; tok = tok->next()) {
509+
if (!Token::simpleMatch(tok, "while ( ! feof ("))
510+
continue;
511+
512+
// Bail out if we cannot identify file pointer
513+
const int fpVarId = tok->tokAt(5)->varId();
514+
if (fpVarId == 0)
515+
continue;
516+
517+
// Usage of feof is correct if a read happens before and within the loop.
518+
// However, if we find a control flow statement in between the fileReadCall
519+
// token and the while loop condition, then we bail out.
520+
const Token *endCond = tok->linkAt(1);
521+
const Token *endBody = endCond->linkAt(1);
522+
523+
const Token *prevFileReadCallTok = findFileReadCall(scope->bodyStart, tok, fpVarId);
524+
const Token *loopFileReadCallTok = findFileReadCall(tok, endBody, fpVarId);
525+
const Token *prevControlFlowTok = Token::findmatch(prevFileReadCallTok, "return|break|goto|continue|throw", tok);
526+
const Token *loopControlFlowTok = Token::findmatch(tok, "return|break|goto|continue|throw", loopFileReadCallTok);
527+
528+
if (prevFileReadCallTok && loopFileReadCallTok && !prevControlFlowTok && !loopControlFlowTok)
529+
continue;
530+
531+
wrongfeofUsage(getCondTok(tok));
532+
}
533+
}
534+
}
535+
536+
void CheckIO::wrongfeofUsage(const Token * tok)
537+
{
538+
reportError(tok, Severity::warning,
539+
"wrongfeofUsage",
540+
"Using feof() as a loop condition causes the last line to be processed twice.\n"
541+
"feof() returns true only after a read has failed due to end-of-file, so the loop "
542+
"body executes once more after the last successful read. Check the return value of "
543+
"the read function instead (e.g. fgets, fread, fscanf).");
544+
}
545+
484546
//---------------------------------------------------------------------------
485547
// printf("%u", "xyz"); // Wrong argument type
486548
// printf("%u%s", 1); // Too few arguments
@@ -2030,6 +2092,7 @@ void CheckIO::runChecks(const Tokenizer &tokenizer, ErrorLogger *errorLogger)
20302092
checkIO.checkWrongPrintfScanfArguments();
20312093
checkIO.checkCoutCerrMisusage();
20322094
checkIO.checkFileUsage();
2095+
checkIO.checkWrongfeofUsage();
20332096
checkIO.invalidScanf();
20342097
}
20352098

@@ -2044,6 +2107,7 @@ void CheckIO::getErrorMessages(ErrorLogger *errorLogger, const Settings *setting
20442107
c.useClosedFileError(nullptr);
20452108
c.seekOnAppendedFileError(nullptr);
20462109
c.incompatibleFileOpenError(nullptr, "tmp");
2110+
c.wrongfeofUsage(nullptr);
20472111
c.invalidScanfError(nullptr);
20482112
c.wrongPrintfScanfArgumentsError(nullptr, "printf",3,2);
20492113
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

@@ -108,6 +111,7 @@ class CPPCHECKLIB CheckIO : public Check {
108111
void seekOnAppendedFileError(const Token *tok);
109112
void incompatibleFileOpenError(const Token *tok, const std::string &filename);
110113
void invalidScanfError(const Token *tok);
114+
void wrongfeofUsage(const Token *tok);
111115
void wrongPrintfScanfArgumentsError(const Token* tok,
112116
const std::string &functionName,
113117
nonneg int numFormat,

releasenotes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ New checks:
88
- MISRA C 2012 rule 10.3 now warns on assigning integer literals 0 and 1 to bool in C99 and later while preserving the existing C89 behavior.
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.
11+
- Warn when feof() is used as a while loop condition (wrongfeofUsage).
1112

1213
C/C++ support:
1314
-

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);
@@ -743,6 +744,36 @@ class TestIO : public TestFixture {
743744
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());
744745
}
745746

747+
void testWrongfeofUsage() { // ticket #958
748+
check("void foo(FILE * fp) {\n"
749+
" while (!feof(fp)) \n"
750+
" {\n"
751+
" char line[100];\n"
752+
" fgets(line, sizeof(line), fp);\n"
753+
" }\n"
754+
"}");
755+
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());
756+
757+
check("int foo(FILE *fp) {\n"
758+
" char line[100];\n"
759+
" while (fgets(line, sizeof(line), fp)) {}\n"
760+
" if (!feof(fp))\n"
761+
" return 1;\n"
762+
" return 0;\n"
763+
"}");
764+
ASSERT_EQUALS("", errout_str());
765+
766+
check("void foo(FILE *fp){\n"
767+
" char line[100];\n"
768+
" fgets(line, sizeof(line), fp);\n"
769+
" while (!feof(fp)){\n"
770+
" dostuff(line);\n"
771+
" fgets(line, sizeof(line), fp);"
772+
" }\n"
773+
"}");
774+
ASSERT_EQUALS("", errout_str());
775+
}
776+
746777

747778
void testScanf1() {
748779
check("void foo() {\n"

0 commit comments

Comments
 (0)