Using the value of a pointer to a FILE
object after the associated file is closed is undefined behavior.
Why is this an issue?
Once a file has been closed, its corresponding FILE*
typed variable becomes invalid and the stream may no longer be accessed through
this variable. In particular, a pointer to a FILE
object may not be passed to fclose
more than once.
Using the value of a pointer to a FILE
object after the associated file is closed results in undefined behavior.
#include <stdio.h>
#include <stdlib.h>
int process_file(int print) {
FILE *f = fopen("example.txt", "r");
if (!f) {
perror("fopen() failed");
return 1;
}
if (print) {
char buffer[256];
while (fgets(buffer, 256, f)) {
printf("%s", buffer);
}
fclose(f);
}
// Further processing ...
fclose(f); // Noncompliant: file associated with `f` might already be closed.
return 0;
}
What is the potential impact?
If a pointer to a FILE
object is used after the associated file is closed, the behavior of the application is undefined.
When a program comprises undefined behavior, the compiler no longer needs to adhere to the language standard, and the program has no meaning
assigned to it. The application might just crash, but in the worst case, the application may appear to execute correctly, while losing data or
producing incorrect results.
How to fix it
Do not use the value of a pointer to a FILE
object after the associated file has been closed.
Code examples
Noncompliant code example
#include <stdio.h>
#include <stdlib.h>
int process_file(int print) {
FILE *f = fopen("example.txt", "r");
if (!f) {
perror("fopen() failed");
return 1;
}
if (print) {
char buffer[256];
while (fgets(buffer, 256, f)) {
printf("%s", buffer);
}
fclose(f);
}
fclose(f); // Noncompliant: file associated with `f` might already be closed.
return 0;
}
Compliant solution
#include <stdio.h>
#include <stdlib.h>
int process_file(int print) {
FILE *f = fopen("example.txt", "r");
if (!f) {
perror("fopen() failed");
return 1;
}
if (print) {
char buffer[256];
while (fgets(buffer, 256, f)) {
printf("%s", buffer);
}
}
if (fclose(f) == EOF) { // Compliant: file associated with `f` is closed only once.
return 1;
}
return 0;
}
Going the extra mile
Using C++'s RAII idiom can mitigate these "double-close" issues.
Following this idiom, one would create a class that manages the underlying file by opening it when the object is constructed and closing it when
the object is destroyed, effectively using a constructor-destructor pair as a "do-undo"-mechanism.
An exemplary class that manages a pointer to a file is shown in what follows.
#include <cstdio>
#include <fstream>
#include <string>
#include <utility>
// Although `std::fstream` should be preferred, if available, a file stream
// managed by this `File` class cannot suffer from "double-close" issues.
class File {
FILE *f;
public:
// Opens file stream on construction.
File(std::string const &path, std::string const &modes)
: f(fopen(path.c_str(), modes.c_str())) {
if (!f) {
throw std::ios_base::failure("fopen() failed");
}
}
// Will close the file stream upon destruction.
~File() {
// Here we are fine with `std::terminate` being called here in case `close`
// throws and exception.
close();
}
// Allow only one owner of a file, disallow copy operations.
File(const File &other) = delete;
File &operator=(const File &other) = delete;
// Moving a file to a different scope shall be allowed.
File(File &&other) : f(std::exchange(other.f, nullptr)) {}
File &operator=(File &&other) {
if (this != &other) {
// In case of non-self-assignment, close the currently managed file and
// "steal" the other's file.
close();
f = std::exchange(other.f, nullptr);
}
return *this;
}
// Allow file to be closed explicitly.
void close() {
if (f != nullptr && fclose(std::exchange(f, nullptr)) == EOF) {
throw std::ios_base::failure("fclose() failed");
}
}
// Allow access to underlying file via `f`.
FILE *handle() { return f; }
// Release `f`, i.e., stop managing it.
FILE *release() { return std::exchange(f, nullptr); }
};
void file_user() {
File fh{"example.txt", "r"};
FILE *f = fh.handle();
// Use `f` for the desired file operation(s).
//
// The file stream managed by `fh` will be automatically closed when `fh` goes
// out of scope at the end of this function.
}
With the design shown above, it is still possible to "double-close" a file by passing the raw FILE
pointer obtained by a call to
File::handle
to fclose
(e.g. fclose(f.handle())
). However, this design reduces the risk of such occurrence by
eliminating the need for manually closing files. If even the reduced possibility of "double-close" is still a concern, the function member
File::handle
should be removed and any required file operations should be wrapped by the File
class.
If falling back to low-level file operations is not necessary, one should prefer std::fstream
.
Resources
Standards
Related rules
- S3520 addresses "double-free" memory issues