ThreadNotifyWallets' block disconnect and connect loops had no try/catch (only its mempool loop does). A wallet database failure inside SyncWithWallets or ChainTip escaped the thread, and TraceThread logs and rethrows, so the node died in std::terminate with nothing flushed. One example: CWalletDB::ListAccountCreditDebit's "error scanning DB" after BDB lost its file to the wolfSSL CloseSocket bug. That is how the pool's node died on 2026-10-12 and 2026-10-16. Run each block's wallet notifications through NotifyWalletsOrShutdown. On an exception it logs the error, requests a shutdown, shows the usual fatal-error message and stops the notifier, leaving pindexLastTip at the last block the wallet fully processed. Carrying on instead would leave the wallet behind the chain, missing the block it failed on. Thread interruption still propagates. The shutdown is requested before the fatal-error message is shown. FdDefense.WalletNotifyFailureShutsDownCleanly: - a throwing notification requests a shutdown, a clean one doesn't; - a non-std exception is handled the same way; - boost::thread_interrupted still propagates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.1 KiB
C++
84 lines
3.1 KiB
C++
// Copyright (c) 2016-2024 The Hush developers
|
|
// Distributed under the GPLv3 software license, see the accompanying
|
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <errno.h>
|
|
#include <stdexcept>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/thread.hpp>
|
|
|
|
#include "chainparams.h"
|
|
#include "init.h"
|
|
#include "main.h"
|
|
#include "noui.h"
|
|
#include "util.h"
|
|
#include "validationinterface.h"
|
|
|
|
extern std::atomic<bool> fRequestShutdown;
|
|
|
|
// A block file opened for reading must not be writable through its fd: a stray write through a reused fd number
|
|
// (the wolfSSL CloseSocket macro let the wallet's BDB fd be closed behind its back) used to land in it.
|
|
TEST(FdDefense, ReadOnlyBlockFileIsNotWritable) {
|
|
SelectParams(CBaseChainParams::MAIN);
|
|
const boost::filesystem::path dir = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("fddefense-%%%%%%%%");
|
|
const bool hadDatadir = mapArgs.count("-datadir") > 0;
|
|
const std::string oldDatadir = hadDatadir ? mapArgs["-datadir"] : "";
|
|
boost::filesystem::create_directories(dir);
|
|
mapArgs["-datadir"] = dir.string();
|
|
ClearDatadirCache();
|
|
|
|
const CDiskBlockPos pos(0, 0);
|
|
const boost::filesystem::path blk = GetBlockPosFilename(pos, "blk");
|
|
boost::filesystem::create_directories(blk.parent_path());
|
|
{
|
|
FILE* f = fopen(blk.string().c_str(), "wb");
|
|
ASSERT_TRUE(f != nullptr);
|
|
fputs("block bytes", f);
|
|
fclose(f);
|
|
}
|
|
|
|
FILE* ro = OpenBlockFile(pos, true);
|
|
ASSERT_TRUE(ro != nullptr);
|
|
char c = 0;
|
|
EXPECT_EQ(1u, fread(&c, 1, 1, ro)) << "still readable";
|
|
errno = 0;
|
|
EXPECT_EQ(-1, pwrite(fileno(ro), "X", 1, 0)) << "a write through a read-only block file's fd must fail";
|
|
EXPECT_EQ(EBADF, errno);
|
|
fclose(ro);
|
|
|
|
FILE* rw = OpenBlockFile(pos, false);
|
|
ASSERT_TRUE(rw != nullptr);
|
|
EXPECT_EQ(1, pwrite(fileno(rw), "b", 1, 0)) << "the writable open still writes";
|
|
fclose(rw);
|
|
|
|
if (hadDatadir) mapArgs["-datadir"] = oldDatadir; else mapArgs.erase("-datadir");
|
|
ClearDatadirCache();
|
|
boost::filesystem::remove_all(dir);
|
|
}
|
|
|
|
// A wallet notification that throws (a wallet DB failure) requests a clean shutdown instead of escaping the
|
|
// notifier thread into std::terminate; a successful one doesn't; thread interruption still propagates.
|
|
TEST(FdDefense, WalletNotifyFailureShutsDownCleanly) {
|
|
noui_connect(); // the daemon connects these UI signals at startup; the error path shows a message box
|
|
fRequestShutdown = false;
|
|
EXPECT_TRUE(NotifyWalletsOrShutdown([] {}));
|
|
EXPECT_FALSE(ShutdownRequested());
|
|
|
|
EXPECT_FALSE(NotifyWalletsOrShutdown([] { throw std::runtime_error("CWalletDB::ListAccountCreditDebit(): error scanning DB"); }));
|
|
EXPECT_TRUE(ShutdownRequested());
|
|
fRequestShutdown = false;
|
|
|
|
EXPECT_FALSE(NotifyWalletsOrShutdown([] { throw 42; }));
|
|
EXPECT_TRUE(ShutdownRequested());
|
|
fRequestShutdown = false;
|
|
|
|
EXPECT_THROW(NotifyWalletsOrShutdown([] { throw boost::thread_interrupted(); }), boost::thread_interrupted);
|
|
EXPECT_FALSE(ShutdownRequested());
|
|
}
|