Modify rule S2095: Expand and adjust for LaYC

## Review

A dedicated reviewer checked the rule description successfully for:

- [ ] logical errors and incorrect information
- [ ] information gaps and missing content
- [ ] text style and tone
- [ ] PR summary and labels follow [the
guidelines](https://github.com/SonarSource/rspec/#to-modify-an-existing-rule)
This commit is contained in:
Philipp Dominik Schubert 2023-08-25 11:40:35 +02:00 committed by GitHub
parent 6c7a0f2fea
commit 1595dcd062
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 169 additions and 100 deletions

View File

@ -1,43 +1,112 @@
A function call to ``++fopen++`` or ``++open++`` must be matched with a call to ``++fclose++`` or ``++close++``, respectively.
== Why is this an issue?
A call to the ``++fopen()++``/``++open()++`` function must be matched with a call to ``++fclose()++``/``++close++``. Otherwise, you run the risk of using up all the OS's file handles, which could lock up not just your program but potentially everything on the box.
The standard C library provides ``++fopen++`` and the system call ``++open++`` to open and possibly create files.
Each call to one of these functions must be matched with a respective call to ``++fclose++`` or ``++close++``.
=== Noncompliant code example
Failing to close files that have been opened may lead to using up all of the OS's file handles.
[source,cpp]
== What is the potential impact?
If a program does not properly close or release file handles after using them, it will leak resources.
In that case, the program will continue to hold onto file handles even when they are no longer needed, eventually exhausting the available file handles.
If a program has run out of file handles and tries to open yet another file, the file opening operation will fail.
This can result in errors or unexpected behavior in the program.
The program may not be able to read or write to files anymore, which can cause data loss, corruption, or incomplete operations.
In some cases, when a program runs out of file handles, it may crash or hang indefinitely.
This can happen if the program does not handle the error condition properly or if it enters an infinite loop trying to open files, for instance.
In the worst case, a resource leak can lock up everything that runs on the machine.
== How to fix it
Make sure that each call to ``++fopen++`` and ``++open++`` has a matching call to ``++fclose++`` and ``++close++``, respectively.
=== Code examples
==== Noncompliant code example
[source,cpp,diff-id=1,diff-type=noncompliant]
----
int fun() {
FILE *f = fopen("file", "r");
if (f == NULL) {
return -1;
#include <stdio.h>
#include <stdlib.h>
int process_file(int print) {
FILE *f = fopen("example.txt", "r");
if (!f) {
perror("fopen() failed");
return 1;
}
// ...
return 0; // Noncompliant, file f has not been closed
if (print) {
char buffer[256];
while (fgets(buffer, 256, f)) {
printf("%s", buffer);
}
}
return 0; // Noncompliant: file `f` has not been closed
}
----
=== Compliant solution
==== Compliant solution
[source,cpp]
[source,cpp,diff-id=1,diff-type=compliant]
----
int fun() {
FILE *f = fopen("file", "r");
if (f == NULL) {
return -1;
#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);
return 0;
return 0; // Compliant: file `f` has been closed
}
----
=== Going the extra mile
Using {cpp}'s _RAII_ idiom can mitigate unmatched calls to ``++fopen++`` and ``++open++``.
include::../../../shared_content/cfamily/c_file_io_raii_extra_mile.adoc[]
With the design shown above, calls to ``++fopen++`` will be automatically matched with a corresponding call to ``++fclose++`` by default.
The associated file will be automatically closed when the ``++File++`` typed file handle object goes out of scope and its destructor is called.
However, it is still possible to leak a resource, if ``++File++``'s function member ``++File::release++`` is used inappropriately.
If this is a concern, this function member should be removed.
If falling back to low-level file operations is not necessary, one should prefer ``++std::fstream++``.
== Resources
* https://cwe.mitre.org/data/definitions/459[MITRE, CWE-459] - Incomplete Cleanup
* https://cwe.mitre.org/data/definitions/772[MITRE, CWE-772] - Missing Release of Resource after Effective Lifetime
* https://wiki.sei.cmu.edu/confluence/x/vjdGBQ[CERT, FIO04-J.] - Release resources when they are no longer needed
* https://wiki.sei.cmu.edu/confluence/x/QtUxBQ[CERT, FIO42-C.] - Close files when they are no longer needed
* https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html[Try With Resources]
=== Standards
* CERT - https://wiki.sei.cmu.edu/confluence/x/vjdGBQ[FIO04-J. Release resources when they are no longer needed]
* CERT - https://wiki.sei.cmu.edu/confluence/x/QtUxBQ[FIO42-C. Close files when they are no longer needed]
* CWE - https://cwe.mitre.org/data/definitions/459[459 Incomplete Cleanup]
* CWE - https://cwe.mitre.org/data/definitions/772[772 Missing Release of Resource after Effective Lifetime]
=== Related rules
* S3588 ensures that ``++FILE*++`` typed variables are not accessed after the associated file has been closed
ifdef::env-github,rspecator-view[]

View File

@ -38,5 +38,5 @@
"defaultQualityProfiles": [
"Sonar way"
],
"quickfix": "unknown"
"quickfix": "infeasible"
}

View File

@ -46,83 +46,6 @@ The application might just crash, but in the worst case, the application may app
Do not use the value of a pointer to a ``++FILE++`` object after the associated file has been closed.
=== Going the extra mile
Using {cpp}'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.
[source,cpp]
----
#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++``.
=== Code examples
==== Noncompliant code example
@ -180,6 +103,18 @@ int process_file(int print) {
}
----
=== Going the extra mile
Using {cpp}'s _RAII_ idiom can mitigate these "double-close" issues.
include::../../../shared_content/cfamily/c_file_io_raii_extra_mile.adoc[]
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

View File

@ -0,0 +1,65 @@
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.
[source,cpp]
----
#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.
}
----