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)
Would it be better if the test mocked the g_dbenvs in db.cpp? I wasn't able to figure out how to do that.
<!--e57a25ab6845829454e8d69fc972939a-->Reviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
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
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 ) {
nit, remove space after ++i.
Fixed
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 =
Use boost::filesystem::path::preferred_separator?
@promag Not sure if you're aware, but anything boost::filesystem:: should use fs:: as is already being done here. See https://github.com/bitcoin/bitcoin/blob/master/src/fs.h.
Yes, just wanted give full type for reference.
Thank you, that's great, fixed!
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\
while (fs::detail::is_directory_separator(env_directory.string().back())) {
env_directory.remove_trailing_separator();
}
Thank you, that's much cleaner, fixed!
Move the path cleanup here? https://github.com/bitcoin/bitcoin/blob/c39fa34bfd90b1de0016ad45c207213bd03ecd75/src/wallet/walletutil.cpp#L12
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.
get_fileid: file ID not set
Running 315 test cases...
unknown 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)
c:\projects\bitcoin\src\wallet\test\psbt_wallet_tests.cpp(18): last checkpoint: "psbt_updater_test" fixture ctor
get_fileid: file ID not set
unknown 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)
c:\projects\bitcoin\src\wallet\test\psbt_wallet_tests.cpp(74): last checkpoint: "parse_hd_keypath" fixture ctor
get_fileid: file ID not set
unknown location(0): fatal error: in "wallet_tests/ComputeTimeSmart": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(230): last checkpoint: "ComputeTimeSmart" fixture ctor
get_fileid: file ID not set
unknown location(0): fatal error: in "wallet_tests/LoadReceiveRequests": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(256): last checkpoint: "LoadReceiveRequests" fixture ctor
get_fileid: file ID not set
unknown location(0): fatal error: in "wallet_tests/ListCoins": class std::runtime_error: BerkeleyBatch: Can't open database wallet.dat (get_fileid failed with 22)
c:\projects\bitcoin\src\wallet\test\wallet_tests.cpp(317): last checkpoint: "ListCoins" fixture ctor
*** 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:
if (gArgs.IsArgSet("-walletdir")) {
path = gArgs.GetArg("-walletdir", "");
boost::system::error_code error;
path = fs::canonical(path, error);
if (error || !fs::is_directory(path)) {
...
fs::canonical checks if it exists and also cleans the path. For instance, locally calling canonical("/Users/promag/.///.//") gives /Users/promag.
Ah 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.
Sounds good, you could replace 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) {
I would add a couple of cases instead.
Took up @promag's excellent suggestion to clean up the path with 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>
Why this include cpp file?
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?
Include 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)
The program will find the implementation by C++ virtual method table?
There is already an instance. Just replace the include.
Ah ok, fixed, I added init.h as well as it has the global instance in it
Appveyor is failing, I rebooted into my Windows environment and see a memory leak is being detected, I'm debugging it now.
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 | +
Remove 2nd empty line.
Fixed
I replaced the wallet/init.cpp include and removed extraneous whitespace, force push diff here https://github.com/bitcoin/bitcoin/commit/c8b2076d6206ac8fe349c08a97611189d2e729a8
I'm still working on the Windows memory leak issue.
I solved the problem on my local Windows machine, the fixture destructor was removing the current working directory - apparently this is ok on posix but not on Windows, force push diff: https://github.com/bitcoin/bitcoin/commit/6e2616deffd815051c1ac2e826bd55850e2898f9
The unit tests is spamming error messages while running test_bitcoin. Would it confuse the user?
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.
@PierreRochard Haven't tested, but can we use gArgs.SoftSetBoolArg("-printtoconsole", false);?
Unfortunately that didn't work. In noui.cpp the noui_ThreadSafeMessageBox function uses fprintf for output
IMO you could just replace it with 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:
AssertionError: [node 0] Expected message "Error: Could not open debug log file \S+$" does not fully match stderr:
""
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>
Please sort includes and make the grouping consistent to how it is done in other files.
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 | +
Nit: Extra newline.
utACK 2d471636eb9160ab51b08e491e3f003f57adbc36 thanks for finding and addressing this bug (and adding tests too !)
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).