Skip to content

Commit

Permalink
Merge bitcoin/bitcoin#31674: init: Lock blocksdir in addition to datadir
Browse files Browse the repository at this point in the history
2656a56 tests: add a test for the new blocksdir lock (Cory Fields)
bdc0a68 init: lock blocksdir in addition to datadir (Cory Fields)
cabb2e5 refactor: introduce a more general LockDirectories for init (Cory Fields)
1db331b init: allow a new xor key to be written if the blocksdir is newly created (Cory Fields)

Pull request description:

  This probably should've been included in #12653 when `-blocksdir` was introduced. Credit TheCharlatan for noticing that it's missing.

  This guards against 2 processes running with separate datadirs but the same blocksdir. I didn't add `walletdir` as I assume sqlite has us covered there.

  It's not likely to happen currently, but may be more relevant in the future with applications using the kernel. Note that the kernel does not currently do any dir locking, but it should.

ACKs for top commit:
  maflcko:
    review ACK 2656a56 🏼
  kevkevinpal:
    ACK [2656a56](bitcoin/bitcoin@2656a56)
  achow101:
    ACK 2656a56
  tdb3:
    Code review and light test ACK 2656a56

Tree-SHA512: 3ba17dc670126adda104148e14d1322ea4f67d671c84aaa9c08c760ef778ca1936832c0dc843cd6367e09939f64c6f0a682b0fa23a5967e821b899dff1fff961
  • Loading branch information
achow101 committed Jan 24, 2025
2 parents 2d07384 + 2656a56 commit 9ecc7af
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 24 deletions.
6 changes: 3 additions & 3 deletions src/bitcoind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,10 @@ static bool AppInit(NodeContext& node)
return InitError(Untranslated("-daemon is not supported on this operating system"));
#endif // HAVE_DECL_FORK
}
// Lock data directory after daemonization
if (!AppInitLockDataDirectory())
// Lock critical directories after daemonization
if (!AppInitLockDirectories())
{
// If locking the data directory failed, exit immediately
// If locking a directory failed, exit immediately
return false;
}
fRet = AppInitInterfaces(node) && AppInitMain(node);
Expand Down
32 changes: 18 additions & 14 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1072,19 +1072,23 @@ bool AppInitParameterInteraction(const ArgsManager& args)
return true;
}

static bool LockDataDirectory(bool probeOnly)
static bool LockDirectory(const fs::path& dir, bool probeOnly)
{
// Make sure only a single Bitcoin process is using the data directory.
const fs::path& datadir = gArgs.GetDataDirNet();
switch (util::LockDirectory(datadir, ".lock", probeOnly)) {
// Make sure only a single process is using the directory.
switch (util::LockDirectory(dir, ".lock", probeOnly)) {
case util::LockResult::ErrorWrite:
return InitError(strprintf(_("Cannot write to data directory '%s'; check permissions."), fs::PathToString(datadir)));
return InitError(strprintf(_("Cannot write to directory '%s'; check permissions."), fs::PathToString(dir)));
case util::LockResult::ErrorLock:
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), fs::PathToString(datadir), CLIENT_NAME));
return InitError(strprintf(_("Cannot obtain a lock on directory %s. %s is probably already running."), fs::PathToString(dir), CLIENT_NAME));
case util::LockResult::Success: return true;
} // no default case, so the compiler can warn about missing cases
assert(false);
}
static bool LockDirectories(bool probeOnly)
{
return LockDirectory(gArgs.GetDataDirNet(), probeOnly) && \
LockDirectory(gArgs.GetBlocksDirPath(), probeOnly);
}

bool AppInitSanityChecks(const kernel::Context& kernel)
{
Expand All @@ -1099,19 +1103,19 @@ bool AppInitSanityChecks(const kernel::Context& kernel)
return InitError(strprintf(_("Elliptic curve cryptography sanity check failure. %s is shutting down."), CLIENT_NAME));
}

// Probe the data directory lock to give an early error message, if possible
// We cannot hold the data directory lock here, as the forking for daemon() hasn't yet happened,
// and a fork will cause weird behavior to it.
return LockDataDirectory(true);
// Probe the directory locks to give an early error message, if possible
// We cannot hold the directory locks here, as the forking for daemon() hasn't yet happened,
// and a fork will cause weird behavior to them.
return LockDirectories(true);
}

bool AppInitLockDataDirectory()
bool AppInitLockDirectories()
{
// After daemonization get the data directory lock again and hold on to it until exit
// After daemonization get the directory locks again and hold on to them until exit
// This creates a slight window for a race condition to happen, however this condition is harmless: it
// will at most make us exit without printing a message to console.
if (!LockDataDirectory(false)) {
// Detailed error printed inside LockDataDirectory
if (!LockDirectories(false)) {
// Detailed error printed inside LockDirectory
return false;
}
return true;
Expand Down
6 changes: 3 additions & 3 deletions src/init.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ bool AppInitParameterInteraction(const ArgsManager& args);
*/
bool AppInitSanityChecks(const kernel::Context& kernel);
/**
* Lock bitcoin core data directory.
* Lock bitcoin core critical directories.
* @note This should only be done after daemonization. Do not call Shutdown() if this function fails.
* @pre Parameters should be parsed and config file should be read, AppInitSanityChecks should have been called.
*/
bool AppInitLockDataDirectory();
bool AppInitLockDirectories();
/**
* Initialize node and wallet interface pointers. Has no prerequisites or side effects besides allocating memory.
*/
bool AppInitInterfaces(node::NodeContext& node);
/**
* Bitcoin core main initialization.
* @note This should only be done after daemonization. Call Shutdown() if this function fails.
* @pre Parameters should be parsed and config file should be read, AppInitLockDataDirectory should have been called.
* @pre Parameters should be parsed and config file should be read, AppInitLockDirectories should have been called.
*/
bool AppInitMain(node::NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info = nullptr);

Expand Down
14 changes: 13 additions & 1 deletion src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,19 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts)
// size of the XOR-key file.
std::array<std::byte, 8> xor_key{};

if (opts.use_xor && fs::is_empty(opts.blocks_dir)) {
// Consider this to be the first run if the blocksdir contains only hidden
// files (those which start with a .). Checking for a fully-empty dir would
// be too aggressive as a .lock file may have already been written.
bool first_run = true;
for (const auto& entry : fs::directory_iterator(opts.blocks_dir)) {
const std::string path = fs::PathToString(entry.path().filename());
if (!entry.is_regular_file() || !path.starts_with('.')) {
first_run = false;
break;
}
}

if (opts.use_xor && first_run) {
// Only use random fresh key when the boolean option is set and on the
// very first start of the program.
FastRandomContext{}.fillrand(xor_key);
Expand Down
2 changes: 1 addition & 1 deletion src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class NodeImpl : public Node
m_context->ecc_context = std::make_unique<ECC_Context>();
if (!AppInitSanityChecks(*m_context->kernel)) return false;

if (!AppInitLockDataDirectory()) return false;
if (!AppInitLockDirectories()) return false;
if (!AppInitInterfaces(*m_context)) return false;

return true;
Expand Down
10 changes: 8 additions & 2 deletions test/functional/feature_filelock.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ def setup_network(self):

def run_test(self):
datadir = self.nodes[0].chain_path
blocksdir = self.nodes[0].blocks_path
self.log.info(f"Using datadir {datadir}")
self.log.info(f"Using blocksdir {blocksdir}")

self.log.info("Check that we can't start a second bitcoind instance using the same datadir")
expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['CLIENT_NAME']} is probably already running."
expected_msg = f"Error: Cannot obtain a lock on directory {datadir}. {self.config['environment']['CLIENT_NAME']} is probably already running."
self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir_path}', '-noserver'], expected_msg=expected_msg)

self.log.info("Check that cookie and PID file are not deleted when attempting to start a second bitcoind using the same datadir")
self.log.info("Check that we can't start a second bitcoind instance using the same blocksdir")
expected_msg = f"Error: Cannot obtain a lock on directory {blocksdir}. {self.config['environment']['CLIENT_NAME']} is probably already running."
self.nodes[1].assert_start_raises_init_error(extra_args=[f'-blocksdir={self.nodes[0].datadir_path}', '-noserver'], expected_msg=expected_msg)

self.log.info("Check that cookie and PID file are not deleted when attempting to start a second bitcoind using the same datadir/blocksdir")
cookie_file = datadir / ".cookie"
assert cookie_file.exists() # should not be deleted during the second bitcoind instance shutdown
pid_file = datadir / BITCOIN_PID_FILENAME_DEFAULT
Expand Down

0 comments on commit 9ecc7af

Please sign in to comment.