If a user passes in a path with a trailing separator as the walletdir
, multiple BerkeleyEnvironments may be created in the same directory which can lead to data corruption.
Discovered while reviewing #12493 (comment)
If a user passes in a path with a trailing separator as the walletdir
, multiple BerkeleyEnvironments may be created in the same directory which can lead to data corruption.
Discovered while reviewing #12493 (comment)
g_dbenvs
in db.cpp? I wasn’t able to figure out how to do that.26+ fs::path data_dir = SetDataDir("get_wallet_env");
27+ fs::path wallet_dir = data_dir / "wallets";
28+
29+ std::vector<fs::path> wallet_test_paths;
30+ std::string trailing_slashes;
31+ for (int i = 0; i < 4; ++i ) {
++i
.
14+/** If a user passes in a path with a trailing separator as the walletdir, multiple BerkeleyEnvironments
15+ * may be created in the same directory which can lead to data corruption.
16+ */
17+BOOST_AUTO_TEST_CASE(get_wallet_env_trailing_slash)
18+{
19+ const char kPathSeparator =
boost::filesystem::path::preferred_separator
?
boost::filesystem::
should use fs::
as is already being done here. See https://github.com/bitcoin/bitcoin/blob/master/src/fs.h.
70@@ -71,8 +71,13 @@ BerkeleyEnvironment* GetWalletEnv(const fs::path& wallet_path, std::string& data
71 env_directory = wallet_path;
72 database_filename = "wallet.dat";
73 }
74+ // Ensure that the directory does not end with a trailing separator to avoid
75+ // creating two Berkeley environments in the same directory
76+ while ((env_directory.string().back() == '/') || (env_directory.string().back() == '\\'))
77+ env_directory = env_directory.remove_trailing_separator();
I would prefer the code down there. It might cause an infinity loop on Unix if the wallet filename is wallet\
0while (fs::detail::is_directory_separator(env_directory.string().back())) {
1 env_directory.remove_trailing_separator();
2}
I’m unable to reproduce the CI failures locally for debugging and the logs don’t seem to provide enough information to pinpoint the problem
You can take a look at appveyor CI. That might be helpful.
0get_fileid: file ID not set
1Running 315 test cases...
2unknown location(0): fatal error: in "psbt_wallet_tests/psbt_updater_test": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
3c:\projects\bitcoin\src\wallet\test\psbt_wallet_tests.cpp(18): last checkpoint: "psbt_updater_test" fixture ctor
4get_fileid: file ID not set
5unknown location(0): fatal error: in "psbt_wallet_tests/parse_hd_keypath": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
6c:\projects\bitcoin\src\wallet\test\psbt_wallet_tests.cpp(74): last checkpoint: "parse_hd_keypath" fixture ctor
7get_fileid: file ID not set
8unknown location(0): fatal error: in "wallet_tests/ComputeTimeSmart": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
9c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(230): last checkpoint: "ComputeTimeSmart" fixture ctor
10get_fileid: file ID not set
11unknown location(0): fatal error: in "wallet_tests/LoadReceiveRequests": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
12c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(256): last checkpoint: "LoadReceiveRequests" fixture ctor
13get_fileid: file ID not set
14unknown location(0): fatal error: in "wallet_tests/ListCoins": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
15c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(317): last checkpoint: "ListCoins" fixture ctor
16*** 5 failures are detected in the test module "Bitcoin Test Suite"
@promag I moved the modification to walletutil.cpp
, excellent suggestion, thank you!
Force push diff: https://github.com/PierreRochard/bitcoin/commit/5a28a99d2887be85d02cb9a9a062f6bde96f56a2
22@@ -23,5 +23,11 @@ fs::path GetWalletDir()
23 }
24 }
25
26+ // Ensure that the directory does not end with a trailing separator to avoid
27+ // creating two Berkeley environments in the same directory
28+ while (fs::detail::is_directory_separator(path.string().back())) {
I think you could add above:
0if (gArgs.IsArgSet("-walletdir")) {
1 path = gArgs.GetArg("-walletdir", "");
2 boost::system::error_code error;
3 path = fs::canonical(path, error);
4 if (error || !fs::is_directory(path)) {
5 ...
fs::canonical
checks if it exists and also cleans the path. For instance, locally calling canonical("/Users/promag/.///.//")
gives /Users/promag
.
fs::canonical
is even better! What do you think about me adding this to WalletInit::Verify
here and then gArgs.ForceSetArg
with the new clean path?
That way we keep the error checking in one place and the path gets cleaned up once, rather than every time GetWalletDir
is called.
fs::exists
with fs::canonical
in https://github.com/bitcoin/bitcoin/blob/465a583f9dcc2383eb20bbb98bffa66fe82f2cde/src/wallet/init.cpp#L182
The nuance with that approach is that we want to avoid transforming a relative file path to an absolute one, per @ryanofsky’s commit message there:
Specifying paths relative to the current working directory in a daemon process can be dangerous, because files can fail to be located even if the configuration doesn’t change, but the daemon is started up differently.
I’m thinking to first check wallet_dir.is_absolute()
and then use fs::canonical
15+ */
16+BOOST_AUTO_TEST_CASE(get_wallet_dir_trailing_separators)
17+{
18+ fs::path expected_wallet_dir = SetDataDir("get_wallet_dir");
19+ std::string trailing_separators;
20+ for (int i = 0; i < 4; ++i) {
fs::canonical
. Added unit tests for the walletdir
interaction in wallet init. If there are other (cross-platform) test cases you would like to see, I’m happy to add them. The commits have completely changed, no value in a force push diff.
6+
7+#include <test/test_bitcoin.h>
8+#include <wallet/test/init_test_fixture.h>
9+
10+#include <wallet/wallet.h>
11+#include <wallet/init.cpp>
The WalletInit
class was moved from the header file to the cpp file in this pull request #12836
Should I move it back or is there a bigger picture architectural issue that I’m not seeing?
src/walletinitinterface.h
?
🤔 how would I create an instance of the WalletInit
class to test its implementation of Verify
?
(I’ll take “read this book / article” as an answer if I’m out of my depth here haha)
init.h
as well as it has the global instance in it
20+ m_walletdir_path_cases["file"] = m_datadir / "not_a_directory.dat";
21+ m_walletdir_path_cases["relative"] = "wallets";
22+ m_walletdir_path_cases["trailing"] = m_datadir / "wallets" / sep;
23+ m_walletdir_path_cases["trailing2"] = m_datadir / "wallets" / sep / sep;
24+
25+
wallet/init.cpp
include and removed extraneous whitespace, force push diff here https://github.com/bitcoin/bitcoin/commit/c8b2076d6206ac8fe349c08a97611189d2e729a8
The unit tests is spamming error messages while running test_bitcoin. Would it confuse the user?
It definitely could, I’ve actually been researching how to properly mock CClientUIInterface
. A shortcut would be to create a unit-test version of noui_connect
but I was struggling on how to capture the output to check it in the unit test. If you have ideas I’m interested, whether they fit into the scope of this PR or not, as I’m sure this isn’t the last time I’ll be hitting InitMessage
in a unit test.
gArgs.SoftSetBoolArg("-printtoconsole", false);
?
noui.cpp
the noui_ThreadSafeMessageBox
function uses fprintf
for output
LogPrintf
. Maybe ask for other dev’s opinion
I just looked into opening a PR for a simple replace of fprintf
with LogPrintf
but I think it would have to be a larger refactoring, as stderr
output is necessary in some cases, for example when the debug log file can’t be opened:
0AssertionError: [node 0] Expected message "Error: Could not open debug log file \S+$" does not fully match stderr:
1""
7+#include <test/test_bitcoin.h>
8+#include <wallet/test/init_test_fixture.h>
9+
10+#include <init.h>
11+#include <walletinitinterface.h>
12+#include <wallet/wallet.h>
5+#ifndef BITCOIN_WALLET_TEST_INIT_TEST_FIXTURE_H
6+#define BITCOIN_WALLET_TEST_INIT_TEST_FIXTURE_H
7+
8+#include <test/test_bitcoin.h>
9+
10+
utACK 2d471636eb9160ab51b08e491e3f003f57adbc36. This fix looks like it does the right thing, but it seems like it would be cleaner and more obvious to do the canonicalization in the GetWalletDir()
function rather than WalletInit::Verify()
.
In particular I don’t like how GetWalletDir()
can return the wrong result if it is called before WalletInit::Verify()
and the right result after. Ideally GetWalletDir()
would just return the same, canonical result all the time, and I’m curious if canonicalizing in GetWalletDir()
wouldn’t work for some reason.
Another more minor issue predating this PR is that WalletInit::Verify()
does the is_absolute
check after checking whether the directory exists, and so might access the filesystem with a path relative to the working directory. Since bitcoind is a daemon, it’s better if it can be agnostic to the working directory, and only access the filesystem with absolute paths (AbsPathForConfigVal
util function is handy for this).