Skip to content

Commit eb5e076

Browse files
committed
wallet: fail migration gracefully on non-writable wallets
Currently, attempting a wallet migration when the database directory or file is not writable results in a filesystem exception that abruptly aborts the process, skipping the post-failure cleanup logic and returning a not particularly helpful error message.
1 parent f76ed32 commit eb5e076

File tree

3 files changed

+47
-0
lines changed

3 files changed

+47
-0
lines changed

src/util/fs_helpers.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <bitcoin-build-config.h> // IWYU pragma: keep
99

1010
#include <logging.h>
11+
#include <random.h>
1112
#include <sync.h>
1213
#include <util/fs.h>
1314
#include <util/syserror.h>
@@ -304,6 +305,28 @@ std::optional<fs::perms> InterpretPermString(const std::string& s)
304305
}
305306
}
306307

308+
bool IsFileWritable(const fs::path& file)
309+
{
310+
if (!fs::exists(file)) throw std::runtime_error(strprintf("File %s does not exist", fs::PathToString(file)));
311+
if (fs::is_directory(file)) throw std::runtime_error(strprintf("Path %s is not a file", fs::PathToString(file)));
312+
std::ofstream ofs(fs::PathToString(file), std::ios::app);
313+
return ofs.is_open();
314+
}
315+
316+
bool IsDirWritable(const fs::path& dir_path) {
317+
// Attempt to create a tmp file in the directory
318+
if (!fs::is_directory(dir_path)) throw std::runtime_error(strprintf("Path %s is not a directory", fs::PathToString(dir_path)));
319+
FastRandomContext rng;
320+
const auto tmp = dir_path / fs::PathFromString(strprintf(".tmp_%d", rng.rand64()));
321+
if (auto created{fsbridge::fopen(tmp, "a")}) {
322+
std::fclose(created);
323+
std::error_code ec;
324+
fs::remove(tmp, ec); // clean up, ignore errors
325+
return true;
326+
}
327+
return false;
328+
}
329+
307330
#ifdef __APPLE__
308331
FSType GetFilesystemType(const fs::path& path)
309332
{

src/util/fs_helpers.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ std::string PermsToSymbolicString(fs::perms p);
9393
*/
9494
std::optional<fs::perms> InterpretPermString(const std::string& s);
9595

96+
/** Check if a file is writable by opening in append mode.
97+
*
98+
* @param[in] file_path path of the file to test
99+
* @throw std::runtime_error if file_path does not exist or is a directory.
100+
*/
101+
bool IsFileWritable(const fs::path& file_path);
102+
103+
/** Check if a directory is writable by creating a temporary file on it.
104+
*
105+
* @param[in] dir_path path of the directory to test
106+
* @return true if a temporary file could be created and removed, false otherwise.
107+
* @throw std::runtime_error if dir_path is not a directory.
108+
*/
109+
bool IsDirWritable(const fs::path& dir_path);
110+
96111
#ifdef WIN32
97112
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
98113
#endif

src/wallet/wallet.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3764,6 +3764,15 @@ bool CWallet::MigrateToSQLite(bilingual_str& error)
37643764
return false;
37653765
}
37663766

3767+
// Check parent directory and db file are writable
3768+
if (m_database->Format() != "mock") {
3769+
const auto file_path = fs::PathFromString(m_database->Filename());
3770+
if (!IsFileWritable(file_path) || !IsDirWritable(file_path.parent_path())) {
3771+
error = _("Error: Wallet db cannot be updated. Adjust directory or file permissions to proceed with migration.");
3772+
return false;
3773+
}
3774+
}
3775+
37673776
// Get all of the records for DB type migration
37683777
std::unique_ptr<DatabaseBatch> batch = m_database->MakeBatch();
37693778
std::unique_ptr<DatabaseCursor> cursor = batch->GetNewCursor();

0 commit comments

Comments
 (0)