Skip to content

Commit

Permalink
util: explicitly close all AutoFiles that have been written
Browse files Browse the repository at this point in the history
There is no way to report a close error from `AutoFile` destructor.
Such an error could be serious if the file has been written to because
it may mean the file is now corrupted (same as if write fails).

So, change all users of `AutoFile` that use it to write data to
explicitly close the file and handle a possible error.
  • Loading branch information
vasild committed Jul 22, 2024
1 parent 8d57361 commit 4533445
Show file tree
Hide file tree
Showing 18 changed files with 90 additions and 19 deletions.
12 changes: 8 additions & 4 deletions src/addrdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <univalue.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/syserror.h>
#include <util/translation.h>

namespace {
Expand Down Expand Up @@ -61,25 +62,28 @@ bool SerializeFileDB(const std::string& prefix, const fs::path& path, const Data
FILE *file = fsbridge::fopen(pathTmp, "wb");
AutoFile fileout{file};
if (fileout.IsNull()) {
fileout.fclose();
remove(pathTmp);
LogError("%s: Failed to open file %s\n", __func__, fs::PathToString(pathTmp));
return false;
}

// Serialize
if (!SerializeDB(fileout, data)) {
fileout.fclose();
(void)fileout.fclose();
remove(pathTmp);
return false;
}
if (!FileCommit(fileout.Get())) {
fileout.fclose();
(void)fileout.fclose();
remove(pathTmp);
LogError("%s: Failed to flush file %s\n", __func__, fs::PathToString(pathTmp));
return false;
}
fileout.fclose();
if (fileout.fclose() != 0) {
remove(pathTmp);
LogError("%s: Failed to close file %s: %s\n", __func__, fs::PathToString(pathTmp), SysErrorString(errno));
return false;
}

// replace existing file, if any, with new file
if (!RenameOver(pathTmp, path)) {
Expand Down
2 changes: 1 addition & 1 deletion src/bench/streams_findbyte.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ static void FindByte(benchmark::Bench& bench)
});

// Cleanup
file.fclose();
(void)file.fclose();
fs::remove("streams_tmp");
}

Expand Down
10 changes: 8 additions & 2 deletions src/index/blockfilterindex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ bool BlockFilterIndex::CustomCommit(CDBBatch& batch)
LogError("%s: Failed to open filter file %d\n", __func__, pos.nFile);
return false;
}
if (!FileCommit(file.Get())) {
if (!FileCommit(file.Get()) || file.fclose() != 0) {
LogError("%s: Failed to commit filter file %d\n", __func__, pos.nFile);
return false;
}
Expand Down Expand Up @@ -205,7 +205,7 @@ size_t BlockFilterIndex::WriteFilterToDisk(FlatFilePos& pos, const BlockFilter&
LogPrintf("%s: Failed to truncate filter file %d\n", __func__, pos.nFile);
return 0;
}
if (!FileCommit(last_file.Get())) {
if (!FileCommit(last_file.Get()) || last_file.fclose() != 0) {
LogPrintf("%s: Failed to commit filter file %d\n", __func__, pos.nFile);
return 0;
}
Expand All @@ -229,6 +229,12 @@ size_t BlockFilterIndex::WriteFilterToDisk(FlatFilePos& pos, const BlockFilter&
}

fileout << filter.GetBlockHash() << filter.GetEncodedFilter();

if (fileout.fclose() != 0) {
LogPrintf("%s: Failed to close filter file %d\n", __func__, pos.nFile);
return 0;
}

return data_size;
}

Expand Down
5 changes: 5 additions & 0 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3943,6 +3943,11 @@ static void CaptureMessageToFile(const CAddress& addr,
uint32_t size = data.size();
ser_writedata32(f, size);
f << data;

if (f.fclose() != 0) {
throw std::ios_base::failure(
strprintf("Error closing %s after write, file contents is likely incomplete", fs::PathToString(path)));
}
}

std::function<void(const CAddress& addr,
Expand Down
10 changes: 10 additions & 0 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,11 @@ bool BlockManager::UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos
hasher << blockundo;
fileout << hasher.GetHash();

if (fileout.fclose() != 0) {
LogError("%s: fclose failed\n", __func__);
return false;
}

return true;
}

Expand Down Expand Up @@ -998,6 +1003,11 @@ bool BlockManager::WriteBlockToDisk(const CBlock& block, FlatFilePos& pos) const
pos.nPos = (unsigned int)fileOutPos;
fileout << TX_WITH_WITNESS(block);

if (fileout.fclose() != 0) {
LogError("WriteBlockToDisk: fclose failed\n");
return false;
}

return true;
}

Expand Down
10 changes: 8 additions & 2 deletions src/node/mempool_persist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/signalinterrupt.h>
#include <util/syserror.h>
#include <util/time.h>
#include <validation.h>

Expand Down Expand Up @@ -168,7 +169,8 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock

auto mid = SteadyClock::now();

AutoFile file{mockable_fopen_function(dump_path + ".new", "wb")};
const fs::path file_fspath{dump_path + ".new"};
AutoFile file{mockable_fopen_function(file_fspath, "wb")};
if (file.IsNull()) {
return false;
}
Expand Down Expand Up @@ -201,7 +203,10 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock

if (!skip_file_commit && !FileCommit(file.Get()))
throw std::runtime_error("FileCommit failed");
file.fclose();
if (file.fclose() != 0) {
throw std::runtime_error(
strprintf("Error closing %s: %s", fs::PathToString(file_fspath), SysErrorString(errno)));
}
if (!RenameOver(dump_path + ".new", dump_path)) {
throw std::runtime_error("Rename failed");
}
Expand All @@ -213,6 +218,7 @@ bool DumpMempool(const CTxMemPool& pool, const fs::path& dump_path, FopenFn mock
fs::file_size(dump_path));
} catch (const std::exception& e) {
LogInfo("Failed to dump mempool: %s. Continuing anyway.\n", e.what());
(void)file.fclose();
return false;
}
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/policy/fees.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ void CBlockPolicyEstimator::Flush() {
void CBlockPolicyEstimator::FlushFeeEstimates()
{
AutoFile est_file{fsbridge::fopen(m_estimation_filepath, "wb")};
if (est_file.IsNull() || !Write(est_file)) {
if (est_file.IsNull() || !Write(est_file) || est_file.fclose() != 0) {
LogPrintf("Failed to write fee estimates to %s. Continue anyway.\n", fs::PathToString(m_estimation_filepath));
} else {
LogPrintf("Flushed fee estimates to %s.\n", fs::PathToString(m_estimation_filepath.filename()));
Expand Down
6 changes: 5 additions & 1 deletion src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include <util/check.h>
#include <util/fs.h>
#include <util/strencodings.h>
#include <util/syserror.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
Expand Down Expand Up @@ -2788,7 +2789,10 @@ UniValue CreateUTXOSnapshot(

CHECK_NONFATAL(written_coins_count == maybe_stats->coins_count);

afile.fclose();
if (afile.fclose() != 0) {
throw std::ios_base::failure(
strprintf("Error closing %s: %s", fs::PathToString(temppath), SysErrorString(errno)));
}

UniValue result(UniValue::VOBJ);
result.pushKV("coins_written", written_coins_count);
Expand Down
1 change: 1 addition & 0 deletions src/streams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@ void AutoFile::write(Span<const std::byte> src)
current_pos += buf_now.size();
}
}
m_was_written = true;
}
26 changes: 23 additions & 3 deletions src/streams.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <serialize.h>
#include <span.h>
#include <support/allocators/zeroafterfree.h>
#include <util/check.h>
#include <util/overflow.h>

#include <algorithm>
Expand Down Expand Up @@ -383,26 +384,45 @@ class BitStreamWriter
*
* 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
{
protected:
std::FILE* m_file;
std::vector<std::byte> m_xor;
bool m_was_written{false};

public:
explicit AutoFile(std::FILE* file, std::vector<std::byte> data_xor={}) : m_file{file}, m_xor{std::move(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 from the destructor we have no way to signal
// error due to close which, after write, could mean the file is
// corrupted and must be handled properly at the call site.
Assume(IsNull());
}

(void)fclose();
}

// Disallow copies
AutoFile(const AutoFile&) = delete;
AutoFile& operator=(const AutoFile&) = delete;

bool feof() const { return std::feof(m_file); }

int fclose()
[[nodiscard]] int fclose()
{
if (auto rel{release()}) return std::fclose(rel);
return 0;
Expand Down
4 changes: 4 additions & 0 deletions src/test/flatfile_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ BOOST_AUTO_TEST_CASE(flatfile_open)
{
AutoFile file{seq.Open(FlatFilePos(0, pos1))};
file << LIMITED_STRING(line1, 256);
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
}

// Attempt to append to file opened in read-only mode.
Expand All @@ -58,6 +59,7 @@ BOOST_AUTO_TEST_CASE(flatfile_open)
{
AutoFile file{seq.Open(FlatFilePos(0, pos2))};
file << LIMITED_STRING(line2, 256);
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
}

// Read text from file in read-only mode.
Expand All @@ -79,13 +81,15 @@ BOOST_AUTO_TEST_CASE(flatfile_open)

file >> LIMITED_STRING(text, 256);
BOOST_CHECK_EQUAL(text, line2);
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
}

// Ensure another file in the sequence has no data.
{
std::string text;
AutoFile file{seq.Open(FlatFilePos(1, pos2))};
BOOST_CHECK_THROW(file >> LIMITED_STRING(text, 256), std::ios_base::failure);
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/test/fuzz/autofile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ FUZZ_TARGET(autofile)
}
},
[&] {
auto_file.fclose();
(void)auto_file.fclose();
},
[&] {
ReadFromStream(fuzzed_data_provider, auto_file);
Expand All @@ -63,5 +63,10 @@ FUZZ_TARGET(autofile)
if (f != nullptr) {
fclose(f);
}
} else {
// FuzzedFileProvider::close() is expected to fail sometimes. Don't let
// the destructor of AutoFile be upset by a failing fclose(). Close it
// explicitly (and ignore any errors) so that the destructor is a noop.
(void)auto_file.fclose();
}
}
1 change: 1 addition & 0 deletions src/test/fuzz/policy_estimator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,6 @@ FUZZ_TARGET(policy_estimator, .init = initialize_policy_estimator)
AutoFile fuzzed_auto_file{fuzzed_file_provider.open()};
block_policy_estimator.Write(fuzzed_auto_file);
block_policy_estimator.Read(fuzzed_auto_file);
(void)fuzzed_auto_file.fclose();
}
}
1 change: 1 addition & 0 deletions src/test/fuzz/policy_estimator_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ FUZZ_TARGET(policy_estimator_io, .init = initialize_policy_estimator_io)
if (block_policy_estimator.Read(fuzzed_auto_file)) {
block_policy_estimator.Write(fuzzed_auto_file);
}
(void)fuzzed_auto_file.fclose();
}
1 change: 1 addition & 0 deletions src/test/fuzz/utxo_snapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ FUZZ_TARGET(utxo_snapshot, .init = initialize_chain)
height++;
}
}
assert(outfile.fclose() == 0);
}

const auto ActivateFuzzedSnapshot{[&] {
Expand Down
8 changes: 5 additions & 3 deletions src/test/streams_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ BOOST_AUTO_TEST_CASE(xor_file)
#endif
AutoFile xor_file{raw_file(mode), xor_pat};
xor_file << test1 << test2;
BOOST_REQUIRE_EQUAL(xor_file.fclose(), 0);
}
{
// Read raw from disk
Expand Down Expand Up @@ -378,8 +379,8 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
// by the rewind window (relative to our farthest read position, 40).
BOOST_CHECK(bf.GetPos() <= 30U);

// We can explicitly close the file, or the destructor will do it.
file.fclose();
// Explicitly close the file and check that the close succeeds.
BOOST_REQUIRE_EQUAL(file.fclose(), 0);

fs::remove(streams_test_filename);
}
Expand Down Expand Up @@ -429,7 +430,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_skip)
bf.SkipTo(13);
BOOST_CHECK_EQUAL(bf.GetPos(), 13U);

file.fclose();
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
fs::remove(streams_test_filename);
}

Expand Down Expand Up @@ -550,6 +551,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
if (maxPos < currentPos)
maxPos = currentPos;
}
BOOST_REQUIRE_EQUAL(file.fclose(), 0);
}
fs::remove(streams_test_filename);
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/util/chainstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ CreateAndActivateUTXOSnapshot(
AutoFile auto_outfile{outfile};

UniValue result = CreateUTXOSnapshot(
node, node.chainman->ActiveChainstate(), auto_outfile, snapshot_path, snapshot_path);
node, node.chainman->ActiveChainstate(), auto_outfile, snapshot_path, snapshot_path); // Will close auto_outfile.
LogPrintf(
"Wrote UTXO snapshot to %s: %s\n", fs::PathToString(snapshot_path.make_preferred()), result.write());

Expand Down
1 change: 1 addition & 0 deletions src/wallet/test/fuzz/wallet_bdb_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ FUZZ_TARGET(wallet_bdb_parser, .init = initialize_wallet_bdb_parser)
{
AutoFile outfile{fsbridge::fopen(wallet_path, "wb")};
outfile << Span{buffer};
assert(outfile.fclose() == 0);
}

const DatabaseOptions options{};
Expand Down

0 comments on commit 4533445

Please sign in to comment.