Skip to content

Commit a01da41

Browse files
committed
Merge bitcoin/bitcoin#29253: wallet: guard against dangling to-be-reverted db transactions
b298242 test: sqlite, add coverage for dangling to-be-reverted db txns (furszy) fc0e747 sqlite: guard against dangling to-be-reverted db transactions (furszy) 472d2ca sqlite: introduce HasActiveTxn method (furszy) dca874e sqlite: add ability to interrupt statements (furszy) fdf9f66 test: wallet db, exercise deadlock after write failure (furszy) Pull request description: Discovered while was reviewing #29112, specifically bitcoin/bitcoin#29112 (review). If the db handler that initiated the database transaction is destroyed, the ongoing transaction cannot be left dangling when the db txn fails to abort. It must be forcefully reverted; otherwise, any subsequent db handler executing a write operation will dump the dangling, to-be-reverted transaction data to disk. This not only breaks the isolation property but also results in the improper storage of incomplete information on disk, impacting the wallet consistency. This PR fixes the issue by resetting the db connection, automatically rolling back the transaction (per https://www.sqlite.org/c3ref/close.html) when the handler object is being destroyed and the txn abortion failed. Testing Notes Can verify the failure by reverting the fix e5217fea and running the test. It will fail without e5217fea and pass with it. ACKs for top commit: achow101: ACK b298242 ryanofsky: Code review ACK b298242. Just fix for exec result codes and comment update since last review. Tree-SHA512: 44ba0323ab21440e79e9d7791bc1c56a8873c8bd3e8f6a85641b91576e1293011fa8032d8ae5b0580f3fb7a949356f7b9676693d7ceffa617aaad9f6569993eb
2 parents 0b76874 + b298242 commit a01da41

File tree

3 files changed

+129
-9
lines changed

3 files changed

+129
-9
lines changed

src/wallet/sqlite.cpp

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,17 @@ void SQLiteDatabase::Close()
377377
m_db = nullptr;
378378
}
379379

380+
bool SQLiteDatabase::HasActiveTxn()
381+
{
382+
// 'sqlite3_get_autocommit' returns true by default, and false if a transaction has begun and not been committed or rolled back.
383+
return m_db && sqlite3_get_autocommit(m_db) == 0;
384+
}
385+
386+
int SQliteExecHandler::Exec(SQLiteDatabase& database, const std::string& statement)
387+
{
388+
return sqlite3_exec(database.m_db, statement.data(), nullptr, nullptr, nullptr);
389+
}
390+
380391
std::unique_ptr<DatabaseBatch> SQLiteDatabase::MakeBatch(bool flush_on_close)
381392
{
382393
// We ignore flush_on_close because we don't do manual flushing for SQLite
@@ -394,12 +405,18 @@ SQLiteBatch::SQLiteBatch(SQLiteDatabase& database)
394405

395406
void SQLiteBatch::Close()
396407
{
397-
// If m_db is in a transaction (i.e. not in autocommit mode), then abort the transaction in progress
398-
if (m_database.m_db && sqlite3_get_autocommit(m_database.m_db) == 0) {
408+
bool force_conn_refresh = false;
409+
410+
// If we began a transaction, and it wasn't committed, abort the transaction in progress
411+
if (m_database.HasActiveTxn()) {
399412
if (TxnAbort()) {
400413
LogPrintf("SQLiteBatch: Batch closed unexpectedly without the transaction being explicitly committed or aborted\n");
401414
} else {
402-
LogPrintf("SQLiteBatch: Batch closed and failed to abort transaction\n");
415+
// If transaction cannot be aborted, it means there is a bug or there has been data corruption. Try to recover in this case
416+
// by closing and reopening the database. Closing the database should also ensure that any changes made since the transaction
417+
// was opened will be rolled back and future transactions can succeed without committing old data.
418+
force_conn_refresh = true;
419+
LogPrintf("SQLiteBatch: Batch closed and failed to abort transaction, resetting db connection..\n");
403420
}
404421
}
405422

@@ -420,6 +437,17 @@ void SQLiteBatch::Close()
420437
}
421438
*stmt_prepared = nullptr;
422439
}
440+
441+
if (force_conn_refresh) {
442+
m_database.Close();
443+
try {
444+
m_database.Open();
445+
} catch (const std::runtime_error&) {
446+
// If open fails, cleanup this object and rethrow the exception
447+
m_database.Close();
448+
throw;
449+
}
450+
}
423451
}
424452

425453
bool SQLiteBatch::ReadKey(DataStream&& key, DataStream& value)
@@ -606,8 +634,8 @@ std::unique_ptr<DatabaseCursor> SQLiteBatch::GetNewPrefixCursor(Span<const std::
606634

607635
bool SQLiteBatch::TxnBegin()
608636
{
609-
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) == 0) return false;
610-
int res = sqlite3_exec(m_database.m_db, "BEGIN TRANSACTION", nullptr, nullptr, nullptr);
637+
if (!m_database.m_db || m_database.HasActiveTxn()) return false;
638+
int res = Assert(m_exec_handler)->Exec(m_database, "BEGIN TRANSACTION");
611639
if (res != SQLITE_OK) {
612640
LogPrintf("SQLiteBatch: Failed to begin the transaction\n");
613641
}
@@ -616,8 +644,8 @@ bool SQLiteBatch::TxnBegin()
616644

617645
bool SQLiteBatch::TxnCommit()
618646
{
619-
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
620-
int res = sqlite3_exec(m_database.m_db, "COMMIT TRANSACTION", nullptr, nullptr, nullptr);
647+
if (!m_database.HasActiveTxn()) return false;
648+
int res = Assert(m_exec_handler)->Exec(m_database, "COMMIT TRANSACTION");
621649
if (res != SQLITE_OK) {
622650
LogPrintf("SQLiteBatch: Failed to commit the transaction\n");
623651
}
@@ -626,8 +654,8 @@ bool SQLiteBatch::TxnCommit()
626654

627655
bool SQLiteBatch::TxnAbort()
628656
{
629-
if (!m_database.m_db || sqlite3_get_autocommit(m_database.m_db) != 0) return false;
630-
int res = sqlite3_exec(m_database.m_db, "ROLLBACK TRANSACTION", nullptr, nullptr, nullptr);
657+
if (!m_database.HasActiveTxn()) return false;
658+
int res = Assert(m_exec_handler)->Exec(m_database, "ROLLBACK TRANSACTION");
631659
if (res != SQLITE_OK) {
632660
LogPrintf("SQLiteBatch: Failed to abort the transaction\n");
633661
}

src/wallet/sqlite.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,21 @@ class SQLiteCursor : public DatabaseCursor
3636
Status Next(DataStream& key, DataStream& value) override;
3737
};
3838

39+
/** Class responsible for executing SQL statements in SQLite databases.
40+
* Methods are virtual so they can be overridden by unit tests testing unusual database conditions. */
41+
class SQliteExecHandler
42+
{
43+
public:
44+
virtual ~SQliteExecHandler() {}
45+
virtual int Exec(SQLiteDatabase& database, const std::string& statement);
46+
};
47+
3948
/** RAII class that provides access to a WalletDatabase */
4049
class SQLiteBatch : public DatabaseBatch
4150
{
4251
private:
4352
SQLiteDatabase& m_database;
53+
std::unique_ptr<SQliteExecHandler> m_exec_handler{std::make_unique<SQliteExecHandler>()};
4454

4555
sqlite3_stmt* m_read_stmt{nullptr};
4656
sqlite3_stmt* m_insert_stmt{nullptr};
@@ -61,6 +71,8 @@ class SQLiteBatch : public DatabaseBatch
6171
explicit SQLiteBatch(SQLiteDatabase& database);
6272
~SQLiteBatch() override { Close(); }
6373

74+
void SetExecHandler(std::unique_ptr<SQliteExecHandler>&& handler) { m_exec_handler = std::move(handler); }
75+
6476
/* No-op. See comment on SQLiteDatabase::Flush */
6577
void Flush() override {}
6678

@@ -142,6 +154,9 @@ class SQLiteDatabase : public WalletDatabase
142154
/** Make a SQLiteBatch connected to this database */
143155
std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override;
144156

157+
/** Return true if there is an on-going txn in this connection */
158+
bool HasActiveTxn();
159+
145160
sqlite3* m_db{nullptr};
146161
bool m_use_unsafe_sync;
147162
};

src/wallet/test/db_tests.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,82 @@ BOOST_AUTO_TEST_CASE(db_cursor_prefix_byte_test)
205205
}
206206
}
207207

208+
BOOST_AUTO_TEST_CASE(db_availability_after_write_error)
209+
{
210+
// Ensures the database remains accessible without deadlocking after a write error.
211+
// To simulate the behavior, record overwrites are disallowed, and the test verifies
212+
// that the database remains active after failing to store an existing record.
213+
for (const auto& database : TestDatabases(m_path_root)) {
214+
// Write original record
215+
std::unique_ptr<DatabaseBatch> batch = database->MakeBatch();
216+
std::string key = "key";
217+
std::string value = "value";
218+
std::string value2 = "value_2";
219+
BOOST_CHECK(batch->Write(key, value));
220+
// Attempt to overwrite the record (expect failure)
221+
BOOST_CHECK(!batch->Write(key, value2, /*fOverwrite=*/false));
222+
// Successfully overwrite the record
223+
BOOST_CHECK(batch->Write(key, value2, /*fOverwrite=*/true));
224+
// Sanity-check; read and verify the overwritten value
225+
std::string read_value;
226+
BOOST_CHECK(batch->Read(key, read_value));
227+
BOOST_CHECK_EQUAL(read_value, value2);
228+
}
229+
}
230+
231+
#ifdef USE_SQLITE
232+
233+
// Test-only statement execution error
234+
constexpr int TEST_SQLITE_ERROR = -999;
235+
236+
class DbExecBlocker : public SQliteExecHandler
237+
{
238+
private:
239+
SQliteExecHandler m_base_exec;
240+
std::set<std::string> m_blocked_statements;
241+
public:
242+
DbExecBlocker(std::set<std::string> blocked_statements) : m_blocked_statements(blocked_statements) {}
243+
int Exec(SQLiteDatabase& database, const std::string& statement) override {
244+
if (m_blocked_statements.contains(statement)) return TEST_SQLITE_ERROR;
245+
return m_base_exec.Exec(database, statement);
246+
}
247+
};
248+
249+
BOOST_AUTO_TEST_CASE(txn_close_failure_dangling_txn)
250+
{
251+
// Verifies that there is no active dangling, to-be-reversed db txn
252+
// after the batch object that initiated it is destroyed.
253+
DatabaseOptions options;
254+
DatabaseStatus status;
255+
bilingual_str error;
256+
std::unique_ptr<SQLiteDatabase> database = MakeSQLiteDatabase(m_path_root / "sqlite", options, status, error);
257+
258+
std::string key = "key";
259+
std::string value = "value";
260+
261+
std::unique_ptr<SQLiteBatch> batch = std::make_unique<SQLiteBatch>(*database);
262+
BOOST_CHECK(batch->TxnBegin());
263+
BOOST_CHECK(batch->Write(key, value));
264+
// Set a handler to prevent txn abortion during destruction.
265+
// Mimicking a db statement execution failure.
266+
batch->SetExecHandler(std::make_unique<DbExecBlocker>(std::set<std::string>{"ROLLBACK TRANSACTION"}));
267+
// Destroy batch
268+
batch.reset();
269+
270+
// Ensure there is no dangling, to-be-reversed db txn
271+
BOOST_CHECK(!database->HasActiveTxn());
272+
273+
// And, just as a sanity check; verify that new batchs only write what they suppose to write
274+
// and nothing else.
275+
std::string key2 = "key2";
276+
std::unique_ptr<SQLiteBatch> batch2 = std::make_unique<SQLiteBatch>(*database);
277+
BOOST_CHECK(batch2->Write(key2, value));
278+
// The first key must not exist
279+
BOOST_CHECK(!batch2->Exists(key));
280+
}
281+
282+
#endif // USE_SQLITE
283+
284+
208285
BOOST_AUTO_TEST_SUITE_END()
209286
} // namespace wallet

0 commit comments

Comments
 (0)