Merge bitcoin/bitcoin#29307: util: explicitly close all AutoFiles that have been written

c10e382d2a flatfile: check whether the file has been closed successfully (Vasil Dimov)
4bb5dd78ea util: check that a file has been closed before ~AutoFile() is called (Vasil Dimov)
8bb34f07df Explicitly close all AutoFiles that have been written (Vasil Dimov)
a69c4098b2 rpc: take ownership of the file by WriteUTXOSnapshot() (Hodlinator)

Pull request description:

  `fclose(3)` may fail to flush the previously written data to disk, thus a failing `fclose(3)` is as serious as a failing `fwrite(3)`.

  Previously the code ignored `fclose(3)` failures. This PR improves that by changing all users of `AutoFile` that use it to write data to explicitly close the file and handle a possible error.

  ---

  Other alternatives are:

  1. `fflush(3)` after each write to the file (and throw if it fails from the `AutoFile::write()` method) and hope that `fclose(3)` will then always succeed. Assert that it succeeds from the destructor 🙄. Will hurt performance.
  2. Throw nevertheless from the destructor. Exception within the exception in C++ I think results in terminating the program without a useful message.
  3. (this is implemented in the latest incarnation of this PR) Redesign `AutoFile` so that its destructor cannot fail. Adjust _all_ its users 😭. For example, if the file has been written to, then require the callers to explicitly call the `AutoFile::fclose()` method before the object goes out of scope. In the destructor, as a sanity check, assume/assert that this is indeed the case. Defeats the purpose of a RAII wrapper for `FILE*` which automatically closes the file when it goes out of scope and there are a lot of users of `AutoFile`.
  4. Pass a new callback function to the `AutoFile` constructor which will be called from the destructor to handle `fclose()` errors, as described in https://github.com/bitcoin/bitcoin/pull/29307#issuecomment-2243842400. My thinking is that if that callback is going to only log a message, then we can log the message directly from the destructor without needing a callback. If the callback is going to do more complicated error handling then it is easier to do that at the call site by directly calling `AutoFile::fclose()` instead of getting the `AutoFile` object out of scope (so that its destructor is called) and inspecting for side effects done by the callback (e.g. set a variable to indicate a failed `fclose()`).

ACKs for top commit:
  l0rinc:
    ACK c10e382d2a
  achow101:
    ACK c10e382d2a
  hodlinator:
    re-ACK c10e382d2a

Tree-SHA512: 3994ca57e5b2b649fc84f24dad144173b7500fc0e914e06291d5c32fbbf8d2b1f8eae0040abd7a5f16095ddf4e11fe1636c6092f49058cda34f3eb2ee536d7ba
This commit is contained in:
Ava Chow committed 2025-07-03 15:37:44 -07:00
commit ea4285775e
20 files changed
+195 -54

No files matched your search

+29 -3
View File
@@ -6,10 +6,13 @@
#ifndef BITCOIN_STREAMS_H
#define BITCOIN_STREAMS_H
#include <logging.h>
#include <serialize.h>
#include <span.h>
#include <support/allocators/zeroafterfree.h>
#include <util/check.h>
#include <util/overflow.h>
#include <util/syserror.h>
#include <algorithm>
#include <cassert>
@@ -387,7 +390,13 @@ public:
*
* Will automatically close the file when it goes out of scope if not null.
* If you're returning the file pointer, return file.release().
* If you need to close the file early, use file.fclose() instead of fclose(file).
* If you need to close the file early, use autofile.fclose() instead of fclose(underlying_FILE).
*
* @note If the file has been written to, then the caller must close it
* explicitly with the `fclose()` method, check if it returns an error and treat
* such an error as if the `write()` method failed. The OS's `fclose(3)` may
* fail to flush to disk data that has been previously written, rendering the
* file corrupt.
*/
class AutoFile
{
@@ -395,11 +404,28 @@ protected:
std::FILE* m_file;
std::vector<std::byte> m_xor;
std::optional<int64_t> m_position;
bool m_was_written{false};
public:
explicit AutoFile(std::FILE* file, std::vector<std::byte> data_xor={});
~AutoFile() { fclose(); }
~AutoFile()
{
if (m_was_written) {
// Callers that wrote to the file must have closed it explicitly
// with the fclose() method and checked that the close succeeded.
// This is because here in the destructor we have no way to signal
// errors from fclose() which, after write, could mean the file is
// corrupted and must be handled properly at the call site.
// Destructors in C++ cannot signal an error to the callers because
// they do not return a value and are not allowed to throw exceptions.
Assume(IsNull());
}
if (fclose() != 0) {
LogError("Failed to close file: %s", SysErrorString(errno));
}
}
// Disallow copies
AutoFile(const AutoFile&) = delete;
@@ -407,7 +433,7 @@ public:
bool feof() const { return std::feof(m_file); }
int fclose()
[[nodiscard]] int fclose()
{
if (auto rel{release()}) return std::fclose(rel);
return 0;