maflcko
commented at 11:28 AM on September 2, 2026:
member
Currently, the index_reorg_crash test may rarely crash due to UB in sanitizers like TSan or ASan. This is perfectly fine, because it is just a rare test-only issue.
However, fix it nonetheless by adding a missing drain of the unused in-flight events. Also, add a small check about the synced state while touching this test.
test: Clarify index.GetSummary().synced state in index_reorg_crash
This clarifies the initial index sync thread is blocked.
faf9c8e8a1
DrahtBot renamed this: test: Avoid unsafe memory race in index_reorg_crash shutdown test: Avoid unsafe memory race in index_reorg_crash shutdown on Sep 2, 2026
DrahtBot added the label Tests on Sep 2, 2026
DrahtBot
commented at 11:28 AM on September 2, 2026:
contributor
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
If your review is incorrectly listed, please copy-paste <code><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--5faf32d7da4f0f540f40219e4f7537a3-->
test: Avoid unsafe memory race in index_reorg_crash shutdown
Without the drain, a BlockConnected event may execute during shutdown
and lead to memory races.
fa0f14ef5e
maflcko added the label CI failed on Sep 2, 2026
maflcko force-pushed on Sep 2, 2026
maflcko
commented at 11:39 AM on September 2, 2026:
member
This is a bit tedious to reproduce, because the failure seems rare. But it happens a bit more often, by adding some sleeps and a stack canary. This diff should fail sanitizers (Asan, Tsan, valgrind) on current master:
diff --git a/src/test/baseindex_tests.cpp b/src/test/baseindex_tests.cpp
index 4e9a03a185..90fa101ca1 100644
--- a/src/test/baseindex_tests.cpp
+++ b/src/test/baseindex_tests.cpp
@@ -164,2 +164,3 @@ private:
std::shared_future<void> m_blocker;
+ std::promise<void> m_callback_entered;
int m_blocking_height;
@@ -178,2 +179,23 @@ public:
+ void WaitForCallbackEntered() { m_callback_entered.get_future().wait(); }
+
+ void BlockConnected(const ChainstateRole& role, const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex) override
+ {
+ if (pindex->nHeight == m_blocking_height + 2) {
+ m_callback_entered.set_value();
+
+ // Ensure the delegated BaseIndex::BlockConnected() passes its
+ // m_synced guard and reaches the callback body.
+ while (!GetSummary().synced) {
+ std::this_thread::sleep_for(1ms);
+ }
+
+ // Keep this callback active while the test destroys the index.
+ // The trailing BaseIndex::BlockConnected() then accesses the
+ // destroyed object.
+ std::this_thread::sleep_for(500ms);
+ }
+ BaseIndex::BlockConnected(role, block, pindex);
+ }
+
bool CustomAppend(const interfaces::BlockInfo& block) override
@@ -198,2 +220,3 @@ BOOST_FIXTURE_TEST_CASE(index_reorg_crash, TestChain100Setup)
+ {
IndexReorgCrash index{interfaces::MakeChain(m_node), blocker, blocking_height, m_clock};
@@ -225,2 +248,5 @@ BOOST_FIXTURE_TEST_CASE(index_reorg_crash, TestChain100Setup)
+ // The index thread is blocked and not done
+ BOOST_CHECK(!index.GetSummary().synced);
+
// Unblock the index thread so it can process the reorg
@@ -229,3 +255,10 @@ BOOST_FIXTURE_TEST_CASE(index_reorg_crash, TestChain100Setup)
func_wait_until(blocking_height + 2, 5s);
+
+ index.WaitForCallbackEntered();
index.Stop();
+ }
+ volatile std::byte stack_buffer[128]{};
+ for (auto& byte : stack_buffer) {
+ byte = std::byte{0xaa};
+ }
}
DrahtBot removed the label CI failed on Sep 2, 2026
sedited approved
sedited
commented at 1:41 PM on September 2, 2026:
contributor
ACKfa0f14ef5e76424ed7770936f7d053f27336a601
test: Avoid unsafe memory race in baseindex_no_commit_ahead_of_flush
Without the drain, a BlockConnected event may execute during shutdown
and lead to memory races.
fab80e82c1
maflcko
commented at 2:34 PM on September 2, 2026:
member
Sorry for the push, but there is another tedious to reproduce race in the other test:
diff --git a/src/index/base.cpp b/src/index/base.cpp
index 5820448..0cbac79 100644
--- a/src/index/base.cpp
+++ b/src/index/base.cpp
@@ -353,2 +353,5 @@ void BaseIndex::BlockConnected(const ChainstateRole& role, const std::shared_ptr
+ // Expand processing time so the test can destroy the index while this event runs.
+ UninterruptibleSleep(55ms);
+
// Ignore BlockConnected signals until we have fully indexed the chain.
diff --git a/src/test/baseindex_tests.cpp b/src/test/baseindex_tests.cpp
index f666f4e..233f85b 100644
--- a/src/test/baseindex_tests.cpp
+++ b/src/test/baseindex_tests.cpp
@@ -35,2 +35,3 @@
+#include <atomic>
#include <chrono>
@@ -63,2 +64,26 @@ BOOST_AUTO_TEST_SUITE(baseindex_tests)
+class BlockConnectedGate final : public CValidationInterface
+{
+public:
+ bool WaitForCallbackAndRelease()
+ {
+ if (m_entered.get_future().wait_for(5s) != std::future_status::ready) return false;
+ m_release.set_value();
+ return true;
+ }
+
+protected:
+ void BlockConnected(const ChainstateRole&, const std::shared_ptr<const CBlock>&, const CBlockIndex*) override
+ {
+ if (!m_armed.exchange(false)) return;
+ m_entered.set_value();
+ m_release.get_future().wait();
+ }
+
+private:
+ std::atomic<bool> m_armed{true};
+ std::promise<void> m_entered;
+ std::promise<void> m_release;
+};
+
// Test that the index does not commit ahead of the chainstate's last
@@ -70,2 +95,7 @@ BOOST_FIXTURE_TEST_CASE(baseindex_no_commit_ahead_of_flush, TestChain100Setup)
Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
+ // TestChain100Setup queued 100 BlockConnected events. Register this first
+ // so it can hold one of those callbacks after CoinStatsIndex has synced.
+ BlockConnectedGate initial_block_connected_gate;
+ m_node.chain->context()->validation_signals->RegisterValidationInterface(&initial_block_connected_gate);
+ bool release_initial_block_connected_gate{true};
for (const auto& [index_name, make_index] : INDEX_FACTORIES) {
@@ -86,2 +116,8 @@ BOOST_FIXTURE_TEST_CASE(baseindex_no_commit_ahead_of_flush, TestChain100Setup)
BOOST_CHECK_EQUAL(index->GetSummary().best_block_height, expected_commit_height);
+ if (!do_flush && release_initial_block_connected_gate) {
+ release_initial_block_connected_gate = false;
+ BOOST_REQUIRE(initial_block_connected_gate.WaitForCallbackAndRelease());
+ // Let the scheduler enter BlockConnected before destroying index.
+ std::this_thread::sleep_for(1ms);
+ }
index->Stop();
@@ -107,2 +143,4 @@ BOOST_FIXTURE_TEST_CASE(baseindex_no_commit_ahead_of_flush, TestChain100Setup)
}
+ m_node.chain->context()->validation_signals->UnregisterValidationInterface(&initial_block_connected_gate);
+ m_node.chain->context()->validation_signals->SyncWithValidationInterfaceQueue();
}
diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp
index 128f14a..7736f98 100644
--- a/src/validationinterface.cpp
+++ b/src/validationinterface.cpp
@@ -17,2 +17,3 @@
#include <util/task_runner.h>
+#include <util/time.h>
@@ -168,2 +169,3 @@ void ValidationSignals::SyncWithValidationInterfaceQueue()
LOG_EVENT("%s", local_log_msg); \
+ UninterruptibleSleep(std::chrono::milliseconds{55}); \
local_event(); \
This should fail with asan and valgrind.
furszy
commented at 2:45 PM on September 2, 2026:
member
ACKfab80e82c1087126477e07eda5f6e3a1f25ceb99
DrahtBot requested review from sedited on Sep 2, 2026
maflcko
commented at 2:50 PM on September 2, 2026:
member
FreeBSD CI failure can be ignored :weary:
DrahtBot added the label CI failed on Sep 2, 2026
arejula27
commented at 8:12 PM on September 2, 2026:
none
ACKfab80e82c1
reproduced the scenario master/pr: index_reorg_crash: fails 20/20 without the patch, 0/20 with it. baseindex_no_commit_ahead_of_flush: fails 19/20 without, 0/20 with, index_unclean_shutdown, blockfilter_index_tests, coinstatsindex_tests and txindex_tests: 0/20 (i did not find more test to fix this).
Nit: update PR descripction, it is outdated (now two test are fixed)
Would an Assume() canary be worth it for future tests? ValidationSignalsImpl::ListEntry already counts executions per subscriber, so exposing that count and checking it in ~BaseIndex() is a few lines. It widens the public API , so I am not sure.
Edit: the whole test_bitcoin suite passes with the canary in place and without forcing the race condition by widening the window, so it catches nothing on a normal run. Usless.
DrahtBot removed the label CI failed on Sep 3, 2026
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2026-09-09 07:56 UTC