wallet: handle non-writable db directories

1) For wallet load, this fixes a crash.

We currently allow loading wallets located on non-writable directories.
This is problematic because the node crashes on any subsequent write.
E.g. generating a block is enough to trigger it.

2) For wallet creation, this improves the returned error msg.

Before: creating a wallet would return a generic error:
"SQLiteDatabase: Failed to open database: unable to open database file"

After: creating a wallet returns:
"SQLiteDatabase: Failed to open database in directory <dir_path>: directory
is not writable"
This commit is contained in:
furszy
2025-12-29 14:23:24 -05:00
parent 0abbe35bb2
commit bc0090f1d6
3 changed files with 38 additions and 1 deletions

View File

@@ -6,8 +6,9 @@
#include <bitcoin-build-config.h> // IWYU pragma: keep
#include <util/fs_helpers.h>
#include <random.h>
#include <sync.h>
#include <tinyformat.h>
#include <util/byte_units.h> // IWYU pragma: keep
#include <util/fs.h>
#include <util/log.h>
@@ -18,6 +19,7 @@
#include <map>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
@@ -306,6 +308,29 @@ std::optional<fs::perms> InterpretPermString(const std::string& s)
}
}
bool IsDirWritable(const fs::path& dir_path)
{
// Attempt to create a tmp file in the directory
if (!fs::is_directory(dir_path)) throw std::runtime_error(strprintf("Path %s is not a directory", fs::PathToString(dir_path)));
FastRandomContext rng;
const auto tmp = dir_path / fs::PathFromString(strprintf(".tmp_%d", rng.rand64()));
const char* mode;
#ifdef __MINGW64__
mode = "w"; // Temporary workaround for https://github.com/bitcoin/bitcoin/issues/30210
#else
mode = "wx";
#endif
if (const auto created{fsbridge::fopen(tmp, mode)}) {
std::fclose(created);
std::error_code ec;
fs::remove(tmp, ec); // clean up, ignore errors
return true;
}
return false;
}
#ifdef __APPLE__
FSType GetFilesystemType(const fs::path& path)
{

View File

@@ -94,6 +94,14 @@ std::string PermsToSymbolicString(fs::perms p);
*/
std::optional<fs::perms> InterpretPermString(const std::string& s);
/** Check if a directory is writable by creating a temporary file on it.
*
* @param[in] dir_path Path of the directory to test
* @return true if a temporary file could be created and removed, false otherwise.
* @throw std::runtime_error if dir_path is not a directory.
*/
bool IsDirWritable(const fs::path& dir_path);
#ifdef WIN32
fs::path GetSpecialFolderPath(int nFolder, bool fCreate = true);
#endif

View File

@@ -257,7 +257,11 @@ void SQLiteDatabase::Open(int additional_flags)
if (m_db == nullptr) {
if (!(flags & SQLITE_OPEN_MEMORY)) {
TryCreateDirectories(m_dir_path);
if (!IsDirWritable(m_dir_path)) {
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database in directory '%s': directory is not writable", fs::PathToString(m_dir_path)));
}
}
int ret = sqlite3_open_v2(m_file_path.c_str(), &m_db, flags, nullptr);
if (ret != SQLITE_OK) {
throw std::runtime_error(strprintf("SQLiteDatabase: Failed to open database: %s\n", sqlite3_errstr(ret)));