Prevent inaccurate missing spans when adding a block
What changed, and why it matters
This commit fixes a race condition in Monero's block synchronization code. When multiple threads check which blocks are missing and write newly received blocks to the database at the same time, one thread can read an outdated database height and wrongly conclude that a block is missing. The patch adds a new mutex to synchronize these checks, reducing false 'missing span' reports that could slow or disrupt syncing. The commit message and comments do not frame this as a security vulnerability, but as a correctness/robustness fix.
Treat as a robustness/correctness improvement rather than an urgent security patch. Reviewers should verify that the new `m_check_span_queue_mutex` does not introduce deadlocks with `m_transactions_lock` or `m_incoming_tx_lock`, as noted in the comments, and consider the larger refactor the developer says is needed.
Security signals we found
Race condition in synchronization state (missing span detection)
New mutex added to serialize span-queue checks with block ingestion
Use of unlocked blockchain query inside synchronization loop
Developer comments warn of remaining races and deadlock risk
Evidence from the diff
The change introduces m_check_span_queue_mutex in cryptonote_protocol_handler and uses it to protect the span-queue check against a race between check_missing_objects/get_next_needed_primitives and the block-add path in handle_notify_new_fluffy_block. The lambda cleanup guard now locks the mutex before cleanup_handle_incoming_blocks, and the missing-span check code locks it around reads of blockchain height and span-queue state. A second change swaps m_core.have_block(...) for m_core.have_block_unlocked(...) in get_next_needed_primitives, apparently to avoid taking a lock inside a loop while the new mutex is held. The comments explicitly note this is a simple, incomplete fix and warn about potential deadlocks with the txpool/incoming-tx locks.
Changed components
src/cryptonote_protocol/cryptonote_protocol_handler.hsrc/cryptonote_protocol/cryptonote_protocol_handler.inlMonero P2P block synchronization / span queue handlingInspect captured patch +25 / −2
diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.h b/src/cryptonote_protocol/cryptonote_protocol_handler.h
index a7dc77c..b79d71b 100644
--- a/src/cryptonote_protocol/cryptonote_protocol_handler.h
+++ b/src/cryptonote_protocol/cryptonote_protocol_handler.h
@@ -193,6 +193,7 @@ namespace cryptonote
std::atomic<bool> m_ask_for_txpool_complement;
boost::mutex m_sync_lock;
block_queue m_block_queue;
+ boost::mutex m_check_span_queue_mutex;
epee::math_helper::once_a_time_seconds<8> m_idle_peer_kicker;
epee::math_helper::once_a_time_milliseconds<100> m_standby_checker;
epee::math_helper::once_a_time_seconds<101> m_sync_search_checker;
diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
index e4c30ad..95c664a 100644
--- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl
+++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
@@ -1489,8 +1489,16 @@ namespace cryptonote
return 1;
}
+ boost::unique_lock<boost::mutex> check_span_lock{m_check_span_queue_mutex, boost::defer_lock};
bool stopped = false;
- epee::unique_scope_guard cleanup_on_exit = [this, &stopped, &context, span_connection_id, start_height]() {
+ epee::unique_scope_guard cleanup_on_exit = [this, &check_span_lock, &stopped, &context, span_connection_id, start_height]() {
+ // Grab the span queue check lock so that we make sure we finish writing the block to the db before checking
+ // the span queue again. Otherwise it's possible for the span queue check to read the db height at n-1 in
+ // thread1, then wait for block n to be added and its span removed from the queue in this thread2, then check
+ // the span queue in thread1 and incorrectly think the span starting at block n is missing.
+ // TODO: a better sync protocol.
+ check_span_lock.lock();
+
if (!m_core.cleanup_handle_incoming_blocks())
{
LOG_PRINT_CCONTEXT_L0("Failure in cleanup_handle_incoming_blocks");
@@ -1619,7 +1627,10 @@ namespace cryptonote
MGINFO_YELLOW("Synced " << current_blockchain_height << "/" << target_blockchain_height
<< progress_message << timing_message);
if (previous_stripe != current_stripe)
+ {
+ check_span_lock.unlock();
notify_new_stripe(context, current_stripe);
+ }
}
}
}
@@ -1964,7 +1975,7 @@ skip:
{
// take out blocks we already have
size_t skip = 0;
- while (skip < context.m_needed_objects.size() && (m_core.have_block(context.m_needed_objects[skip].first) || (check_block_queue && m_block_queue.have(context.m_needed_objects[skip].first))))
+ while (skip < context.m_needed_objects.size() && (m_core.have_block_unlocked(context.m_needed_objects[skip].first) || (check_block_queue && m_block_queue.have(context.m_needed_objects[skip].first))))
{
// if we're popping the last hash, record it so we can ask again from that hash,
// this prevents never being able to progress on peers we get old hash lists from
@@ -2020,6 +2031,15 @@ skip:
{
do
{
+ // Enforce synchronization when checking the span queue. This is a simple solution to prevent unexpected races
+ // when checking the span queue. It's not ideal and doesn't fully solve all possible races.
+ // This section largely needs to be reworked.
+ // Warning: make sure to unlock this to avoid deadlocks if necessary
+ // If any of the functions below acquire the txpool lock (m_transactions_lock) or m_incoming_tx_lock, we can
+ // deadlock, since m_core.prepare_handle_incoming_blocks acquires both and does not release until
+ // m_core.cleanup_handle_incoming_blocks.
+ boost::unique_lock<boost::mutex> check_span_lock{m_check_span_queue_mutex};
+
const size_t nspans = m_block_queue.get_num_filled_spans();
const size_t size = m_block_queue.get_data_size();
const uint64_t bc_height = m_core.get_current_blockchain_height();
@@ -2039,6 +2059,7 @@ skip:
}
MDEBUG(context << "Nothing to get from this peer, and it's not ahead of us, all done");
context.set_state_normal();
+ check_span_lock.unlock();
if (m_core.get_current_blockchain_height() >= m_core.get_target_blockchain_height())
on_connection_synchronized();
return true;
@@ -2110,6 +2131,7 @@ skip:
MLOG_PEER_STATE("resuming");
context.m_state = cryptonote_connection_context::state_standby;
++context.m_callback_request_count;
+ check_span_lock.unlock();
m_p2p->request_callback(context);
return true;
}
Why this scored 34/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.