Skip to content

Commit 978b9c5

Browse files
Fix #1473: warn when fclose() is used as a while loop condition (#8444)
Using fclose() as a while loop condition closes the file on every iteration and operates on an already-closed file handle from the second iteration onward. --------- Signed-off-by: Francois Berder <fberder@outlook.fr>
1 parent 39d9c01 commit 978b9c5

5 files changed

Lines changed: 97 additions & 1 deletion

File tree

lib/checkio.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,28 @@ void CheckIO::checkFileUsage()
240240
} else if (tok->str() == "fclose") {
241241
fileTok = tok->tokAt(2);
242242
operation = Filepointer::Operation::CLOSE;
243+
244+
// #1473 Check if fclose is in a while loop condition
245+
if (fileTok && fileTok->isVariable()) {
246+
const Token* loopTok = tok->astTop()->previous();
247+
248+
if (loopTok && loopTok->str() == "while") {
249+
const Token* bodyEnd = nullptr;
250+
const Token* bodyStart = nullptr;
251+
252+
if (Token::simpleMatch(loopTok->previous(), "}") && loopTok->previous()->scope()->type == ScopeType::eDo) { // Handle do-while loops
253+
bodyEnd = loopTok->previous();
254+
bodyStart = bodyEnd->link();
255+
} else {
256+
bodyStart = loopTok->linkAt(1)->next();
257+
bodyEnd = bodyStart->link();
258+
}
259+
260+
// Do not trigger a warning if the loop always exits or if the file is opened again in the loop.
261+
if (!isReturnScope(bodyEnd, mSettings->library) && Token::findmatch(bodyStart, "%var% =", bodyEnd, fileTok->varId()) == nullptr)
262+
fcloseInLoopConditionError(tok, fileTok->str());
263+
}
264+
}
243265
} else if (whitelist.find(tok->str()) != whitelist.end()) {
244266
fileTok = tok->tokAt(2);
245267
if ((tok->str() == "ungetc" || tok->str() == "ungetwc") && fileTok)
@@ -387,6 +409,15 @@ void CheckIO::useClosedFileError(const Token *tok)
387409
"useClosedFile", "Used file that is not opened.", CWE910, Certainty::normal);
388410
}
389411

412+
void CheckIO::fcloseInLoopConditionError(const Token *tok, const std::string &varname)
413+
{
414+
reportError(tok, Severity::warning,
415+
"fcloseInLoopCondition",
416+
"fclose() used as loop condition may skip loop body or double-close file handle.\n"
417+
"fclose() closes '" + varname + "' each time it is evaluated. On success the loop body might never execute, on failure fclose() might be called again on the already-closed file handle.",
418+
CWE910, Certainty::normal);
419+
}
420+
390421
void CheckIO::seekOnAppendedFileError(const Token *tok)
391422
{
392423
reportError(tok, Severity::warning,
@@ -2038,6 +2069,7 @@ void CheckIO::getErrorMessages(ErrorLogger *errorLogger, const Settings *setting
20382069
c.readWriteOnlyFileError(nullptr);
20392070
c.writeReadOnlyFileError(nullptr);
20402071
c.useClosedFileError(nullptr);
2072+
c.fcloseInLoopConditionError(nullptr, "fp");
20412073
c.seekOnAppendedFileError(nullptr);
20422074
c.incompatibleFileOpenError(nullptr, "tmp");
20432075
c.invalidScanfError(nullptr);

lib/checkio.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class CPPCHECKLIB CheckIO : public Check {
105105
void readWriteOnlyFileError(const Token *tok);
106106
void writeReadOnlyFileError(const Token *tok);
107107
void useClosedFileError(const Token *tok);
108+
void fcloseInLoopConditionError(const Token *tok, const std::string &varname);
108109
void seekOnAppendedFileError(const Token *tok);
109110
void incompatibleFileOpenError(const Token *tok, const std::string &filename);
110111
void invalidScanfError(const Token *tok);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# fcloseInLoopCondition
2+
3+
**Message**: fclose() used as loop condition may skip loop body or double-close file handle.<br/>
4+
**Category**: Resource Management<br/>
5+
**Severity**: Warning<br/>
6+
**Language**: C and C++
7+
8+
## Description
9+
10+
Using `fclose()` as a loop condition leads to two unwanted outcomes:
11+
12+
- On **success**, the condition is false and the loop body never executes. The intent was likely to process the file inside the loop, but it is already closed.
13+
- On **failure**, the condition is true and the loop body executes but `fclose()` is called again on the already-closed file handle on the next iteration, which is undefined behaviour.
14+
15+
This pattern is almost always a misunderstanding of what `fclose()` returns, or confusion with a function that reads/processes data incrementally (like `fgets` or `fread`). Unlike those functions, `fclose()` is a one-shot teardown operation and has no meaningful retry or loop-until-done semantic.
16+
17+
## How to fix
18+
19+
Call `fclose()` outside the loop condition. If you need to check whether the close succeeded, store the return value and test it separately.
20+
21+
Before:
22+
```c
23+
FILE *fp = fopen("data.txt", "r");
24+
while (fclose(fp)) {
25+
/* process file */
26+
}
27+
```
28+
29+
After:
30+
```c
31+
FILE *fp = fopen("data.txt", "r");
32+
/* process file */
33+
if (fclose(fp) != 0) {
34+
/* handle close error */
35+
}
36+
```
37+
38+
## Related checkers
39+
40+
- `useClosedFile` - for using a file handle that has already been closed

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+
- fcloseInLoopCondition warns when fclose() is used as a while loop condition, which may skip the loop body or double-close the file handle.
1112

1213
C/C++ support:
1314
-

test/testio.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,29 @@ class TestIO : public TestFixture {
545545
" FILE *a = fopen(\"aa\", \"r\");\n"
546546
" while (fclose(a)) {}\n"
547547
"}");
548-
TODO_ASSERT_EQUALS("[test.cpp:3:5]: (error) Used file that is not opened. [useClosedFile]\n", "", errout_str());
548+
ASSERT_EQUALS("[test.cpp:3:12]: (warning) fclose() used as loop condition may skip loop body or double-close file handle. [fcloseInLoopCondition]\n", errout_str());
549+
550+
check("void foo() {\n"
551+
" FILE *a = fopen(\"aa\", \"r\");\n"
552+
" while (fclose(a)) {\n"
553+
" break;\n"
554+
" }\n"
555+
"}");
556+
ASSERT_EQUALS("", errout_str());
557+
558+
check("void foo() {\n"
559+
" FILE *a = fopen(\"aa\", \"r\");\n"
560+
" while (fclose(a)) {\n"
561+
" a = fopen(\"aa\", \"r\");\n"
562+
" }\n"
563+
"}");
564+
ASSERT_EQUALS("", errout_str());
565+
566+
check("void foo() {\n"
567+
" FILE *a = fopen(\"aa\", \"r\");\n"
568+
" do {} while (fclose(a));\n"
569+
"}");
570+
ASSERT_EQUALS("[test.cpp:3:18]: (warning) fclose() used as loop condition may skip loop body or double-close file handle. [fcloseInLoopCondition]\n", errout_str());
549571

550572
// #6823
551573
check("void foo() {\n"

0 commit comments

Comments
 (0)