wallet: use in-memory SQLite for temporary wallet in exportwatchonlywallet
What changed, and why it matters
This change refactors how Bitcoin Core creates a temporary wallet during the exportwatchonlywallet command. Previously, the temporary wallet was written to disk in the wallets directory and then deleted afterward. Now it is kept entirely in memory using SQLite's in-memory mode, so no temporary files are created on disk. This is a cleanup and hardening improvement, not a fix for an active exploit.
No urgent action required. This is a defensive refactoring. Reviewers may want to confirm that BackupWallet() still works correctly when copying from an in-memory source and that concurrent exports of wallets with the same name remain safe (the patch names the in-memory wallet after the source wallet to keep concurrent exports distinct).
Security signals we found
Eliminates temporary wallet files written to disk during exportwatchonlywallet
Removes manual filesystem cleanup path that ran on both success and failure
Reduces attack surface related to leftover or partially-written wallet files
No change to cryptographic handling, key material, or network behavior
Evidence from the diff
The patch introduces InMemoryWalletDatabase and MakeInMemoryWalletDatabase() in the SQLite wallet database layer, and uses them in ExportWatchOnlyWallet() instead of creating a real on-disk SQLite database and cleaning it up afterward. The temporary watchonly wallet is now backed by an in-memory SQLite database (SQLITE_OPEN_MEMORY with path ‘:memory:’), eliminating disk I/O and a manual file-cleanup handler. The test helper MockableSQLiteDatabase is refactored to inherit from InMemoryWalletDatabase, removing its duplicated Files() override.
Changed components
src/wallet/export.cppsrc/wallet/sqlite.cppsrc/wallet/sqlite.hsrc/wallet/test/util.cppsrc/wallet/test/util.hInspect captured patch +26 / −31
diff --git a/src/wallet/export.cpp b/src/wallet/export.cpp
index 6f170dd7..1df51b6b 100644
--- a/src/wallet/export.cpp
+++ b/src/wallet/export.cpp
@@ -9,6 +9,7 @@
#include <util/expected.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/context.h>
+#include <wallet/sqlite.h>
#include <wallet/wallet.h>
#include <fstream>
@@ -69,40 +70,15 @@ util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs:
return util::Error{_("Error: Wallet has no descriptors to export")};
}
- // Setup DatabaseOptions to create a new sqlite database
- DatabaseOptions options;
- options.require_existing = false;
- options.require_create = true;
- options.require_format = DatabaseFormat::SQLITE;
-
// Make the wallet with the same flags as this wallet, but without private keys
- options.create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
+ const uint64_t create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
- // Make the watchonly wallet
- DatabaseStatus status;
+ // Create the temporary watchonly wallet in memory to avoid leaving files on disk
std::vector<bilingual_str> warnings;
- std::string wallet_name = wallet.GetName() + "_watchonly_temp";
bilingual_str error;
- std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
- if (!database) {
- return util::Error{strprintf(_("Wallet file creation failed: %s"), error)};
- }
-
- // Always remove the temporary wallet files, even when returning early on error.
- std::shared_ptr<CWallet> watchonly_wallet;
- fs::path wallet_path = fs::PathFromString(database->Filename()).parent_path();
- std::vector<fs::path> cleanup_files = database->Files();
- auto cleanup_watchonly_wallet = interfaces::MakeCleanupHandler([&watchonly_wallet, &wallet_path, &cleanup_files] {
- if (watchonly_wallet) watchonly_wallet.reset();
- for (const auto& file : cleanup_files) {
- fs::remove(file);
- }
- fs::remove(wallet_path);
- });
-
WalletContext empty_context;
empty_context.args = context.args;
- watchonly_wallet = CWallet::CreateNew(empty_context, wallet_name, std::move(database), options.create_flags, /*born_encrypted=*/false, error, warnings);
+ std::shared_ptr<CWallet> watchonly_wallet = CWallet::CreateNew(empty_context, /*name=*/wallet.GetName() + "_watchonly_temp", MakeInMemoryWalletDatabase(), create_flags, /*born_encrypted=*/false, error, warnings);
if (!watchonly_wallet) {
return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)};
}
diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp
index 28123ecf..fa2abc93 100644
--- a/src/wallet/sqlite.cpp
+++ b/src/wallet/sqlite.cpp
@@ -722,6 +722,15 @@ std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const D
}
}
+InMemoryWalletDatabase::InMemoryWalletDatabase()
+ : SQLiteDatabase(fs::path{}, fs::path{":memory:"}, DatabaseOptions(), SQLITE_OPEN_MEMORY)
+{}
+
+std::unique_ptr<WalletDatabase> MakeInMemoryWalletDatabase()
+{
+ return std::make_unique<InMemoryWalletDatabase>();
+}
+
std::string SQLiteDatabaseVersion()
{
return std::string(sqlite3_libversion());
diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h
index 3a35bb24..f3268b4b 100644
--- a/src/wallet/sqlite.h
+++ b/src/wallet/sqlite.h
@@ -175,8 +175,19 @@ public:
bool m_use_unsafe_sync;
};
+/** An in-memory SQLiteDatabase. Used as a temporary build artifact where no
+ * on-disk persistence is needed. */
+class InMemoryWalletDatabase : public SQLiteDatabase
+{
+public:
+ InMemoryWalletDatabase();
+ std::vector<fs::path> Files() override { return {}; }
+};
+
std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error);
+std::unique_ptr<WalletDatabase> MakeInMemoryWalletDatabase();
+
std::string SQLiteDatabaseVersion();
} // namespace wallet
diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp
index 43cfd40f..19281be6 100644
--- a/src/wallet/test/util.cpp
+++ b/src/wallet/test/util.cpp
@@ -116,7 +116,7 @@ CTxDestination getNewDestination(CWallet& w, OutputType output_type)
}
MockableSQLiteDatabase::MockableSQLiteDatabase()
- : SQLiteDatabase(fs::PathFromString("mock/"), fs::PathFromString("mock/wallet.dat"), DatabaseOptions(), SQLITE_OPEN_MEMORY)
+ : InMemoryWalletDatabase()
{}
std::unique_ptr<WalletDatabase> CreateMockableWalletDatabase()
diff --git a/src/wallet/test/util.h b/src/wallet/test/util.h
index 9a407d31..0af46e55 100644
--- a/src/wallet/test/util.h
+++ b/src/wallet/test/util.h
@@ -56,7 +56,7 @@ public:
/** A WalletDatabase whose contents and return values can be modified as needed for testing
**/
-class MockableSQLiteDatabase : public SQLiteDatabase
+class MockableSQLiteDatabase : public InMemoryWalletDatabase
{
public:
MockableSQLiteDatabase();
@@ -64,7 +64,6 @@ public:
bool Backup(const std::string& strDest) const override { return true; }
std::string Filename() override { return "mockable"; }
- std::vector<fs::path> Files() override { return {}; }
std::string Format() override { return "sqlite-mock"; }
std::unique_ptr<DatabaseBatch> MakeBatch() override { return std::make_unique<MockableSQLiteBatch>(*this); }
};
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.