diff --git a/src/wallet/db.h b/src/wallet/db.h --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -20,11 +20,17 @@ #include #include #include +#include #include static const unsigned int DEFAULT_WALLET_DBLOGSIZE = 100; static const bool DEFAULT_WALLET_PRIVDB = true; +struct WalletDatabaseFileId { + u_int8_t value[DB_FILE_ID_LEN]; + bool operator==(const WalletDatabaseFileId &rhs) const; +}; + class BerkeleyEnvironment { private: bool fDbEnvInit; @@ -38,6 +44,7 @@ std::unique_ptr dbenv; std::map mapFileUseCount; std::map mapDb; + std::unordered_map m_fileids; std::condition_variable_any m_db_in_use; BerkeleyEnvironment(const fs::path &env_directory); diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -31,32 +31,28 @@ //! so bitcoin should never create different databases with the same fileid, but //! this error can be triggered if users manually copy database files. void CheckUniqueFileid(const BerkeleyEnvironment &env, - const std::string &filename, Db &db) { + const std::string &filename, Db &db, + WalletDatabaseFileId &fileid) { if (env.IsMock()) { return; } - u_int8_t fileid[DB_FILE_ID_LEN]; - int ret = db.get_mpf()->get_fileid(fileid); + int ret = db.get_mpf()->get_fileid(fileid.value); if (ret != 0) { throw std::runtime_error(strprintf( "BerkeleyBatch: Can't open database %s (get_fileid failed with %d)", filename, ret)); } - for (const auto &item : env.mapDb) { - u_int8_t item_fileid[DB_FILE_ID_LEN]; - if (item.second && - item.second->get_mpf()->get_fileid(item_fileid) == 0 && - memcmp(fileid, item_fileid, sizeof(fileid)) == 0) { - const char *item_filename = nullptr; - item.second->get_dbname(&item_filename, nullptr); + for (const auto &item : env.m_fileids) { + if (fileid == item.second && &fileid != &item.second) { throw std::runtime_error(strprintf( "BerkeleyBatch: Can't open database %s (duplicates fileid %s " "from %s)", filename, - HexStr(std::begin(item_fileid), std::end(item_fileid)), - item_filename ? item_filename : "(unknown database)")); + HexStr(std::begin(item.second.value), + std::end(item.second.value)), + item.first)); } } } @@ -67,6 +63,10 @@ std::map g_dbenvs; } // namespace +bool WalletDatabaseFileId::operator==(const WalletDatabaseFileId &rhs) const { + return memcmp(value, &rhs.value, sizeof(value)) == 0; +} + BerkeleyEnvironment *GetWalletEnv(const fs::path &wallet_path, std::string &database_filename) { fs::path env_directory; @@ -555,7 +555,8 @@ // versions of BDB have an set_lk_exclusive method for this // purpose, but the older version we use does not.) for (const auto &dbenv : g_dbenvs) { - CheckUniqueFileid(dbenv.second, strFilename, *pdb_temp); + CheckUniqueFileid(dbenv.second, strFilename, *pdb_temp, + this->env->m_fileids[strFilename]); } pdb = pdb_temp.release(); @@ -909,6 +910,14 @@ LOCK(cs_db); g_dbenvs.erase(env->Directory().string()); env = nullptr; + } else { + // TODO: To avoid g_dbenvs.erase erasing the environment prematurely + // after the first database shutdown when multiple databases are + // open in the same environment, should replace raw database `env` + // pointers with shared or weak pointers, or else separate the + // database and environment shutdowns so environments can be shut + // down after databases. + env->m_fileids.erase(strFile); } } } diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -225,6 +225,11 @@ assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid", self.nodes[0].loadwallet, 'w8_copy') + # Fail to load if one wallet is a copy of another. + # Test this twice to make sure that we don't re-introduce https://github.com/bitcoin/bitcoin/issues/14304 + assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid", + self.nodes[0].loadwallet, 'w8_copy') + # Fail to load if wallet file is a symlink assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'", self.nodes[0].loadwallet, 'w8_symlink')