Skip to content

Commit e3bf6a5

Browse files
committed
Fix race condition in async UtxoFuture resolution
Previously, we refactored the `GossipVerifier` to not require holding a circular reference. As part of this, we moved to a model where the `UtxoFuture`s are now polled by the background processor which checks for completion through `get_and_clear_pending_msg_events`. However, as part of this refactor we introduced race-condition: as we only held `Weak` references in `PendingChecksContext` and the `UtxoFuture` was directly dropped by the `GossipVerifier` after calling `resolve`, the actual data was dropped with the future and gone when the background processor attempted to retrieve and apply it via `check_resolved_futures`. Here, we fix this issue by simply holding on to the `state` `Arc`s in a separate `pending_states` `Vec` that is only pruned in `check_resolved_futures`, ensuring any completed results are collected first.
1 parent 1fafc83 commit e3bf6a5

1 file changed

Lines changed: 31 additions & 22 deletions

File tree

lightning/src/routing/utxo.rs

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ struct UtxoMessages {
126126

127127
/// Represents a future resolution of a [`UtxoLookup::get_utxo`] query resolving async.
128128
///
129-
/// See [`UtxoResult::Async`] and [`UtxoFuture::resolve`] for more info.
129+
/// See [`UtxoResult::Async`jut] and [`UtxoFuture::resolve`] for more info.
130130
#[derive(Clone)]
131131
pub struct UtxoFuture {
132132
state: Arc<Mutex<UtxoMessages>>,
@@ -166,6 +166,7 @@ impl UtxoFuture {
166166
}
167167

168168
struct PendingChecksContext {
169+
pending_states: Vec<Arc<Mutex<UtxoMessages>>>,
169170
channels: HashMap<u64, Weak<Mutex<UtxoMessages>>>,
170171
nodes: HashMap<NodeId, Vec<Weak<Mutex<UtxoMessages>>>>,
171172
}
@@ -180,6 +181,7 @@ impl PendingChecks {
180181
pub(super) fn new() -> Self {
181182
PendingChecks {
182183
internal: Mutex::new(PendingChecksContext {
184+
pending_states: Vec::new(),
183185
channels: new_hash_map(),
184186
nodes: new_hash_map(),
185187
}),
@@ -413,6 +415,18 @@ impl PendingChecks {
413415
// handle the result in-line.
414416
handle_result(res)
415417
} else {
418+
{
419+
let pending_states = &mut pending_checks.pending_states;
420+
if pending_states
421+
.iter()
422+
.find(|s| Arc::ptr_eq(s, &future.state))
423+
.is_none()
424+
{
425+
// We're not already tracking the future state, keep the `Arc`
426+
// around at least until next call of `check_resolved_futures`.
427+
pending_states.push(Arc::clone(&future.state));
428+
}
429+
}
416430
Self::check_replace_previous_entry(
417431
msg,
418432
full_msg,
@@ -574,33 +588,24 @@ impl PendingChecks {
574588
let mut completed_states = Vec::new();
575589
{
576590
let mut lck = self.internal.lock().unwrap();
577-
lck.channels.retain(|_, state| {
578-
if let Some(state) = state.upgrade() {
579-
if state.lock().unwrap().complete.is_some() {
580-
completed_states.push(state);
591+
lck.pending_states.retain(|state| {
592+
if state.lock().unwrap().complete.is_some() {
593+
// We're done, collect the result and clean up.
594+
completed_states.push(Arc::clone(&state));
595+
false
596+
} else {
597+
if Arc::strong_count(state) == 1 {
598+
// The future has been dropped.
581599
false
582600
} else {
601+
// It's still inflight.
583602
true
584603
}
585-
} else {
586-
// The UtxoFuture has been dropped, drop the pending-lookup state.
587-
false
588604
}
589605
});
606+
lck.channels.retain(|_, state| Weak::strong_count(state) > 0);
590607
lck.nodes.retain(|_, lookups| {
591-
lookups.retain(|state| {
592-
if let Some(state) = state.upgrade() {
593-
if state.lock().unwrap().complete.is_some() {
594-
completed_states.push(state);
595-
false
596-
} else {
597-
true
598-
}
599-
} else {
600-
// The UtxoFuture has been dropped, drop the pending-lookup state.
601-
false
602-
}
603-
});
608+
lookups.retain(|state| Weak::strong_count(state) > 0);
604609
!lookups.is_empty()
605610
});
606611
}
@@ -1081,8 +1086,12 @@ mod tests {
10811086
assert!(network_graph.pending_checks.too_many_checks_pending());
10821087

10831088
// Once the future is drop'd (by resetting the `utxo_ret` value) the "too many checks" flag
1084-
// should reset to false.
1089+
// should not yet reset to false.
10851090
*chain_source.utxo_ret.lock().unwrap() = UtxoResult::Sync(Err(UtxoLookupError::UnknownTx));
1091+
assert!(network_graph.pending_checks.too_many_checks_pending());
1092+
1093+
// .. but it should once we called check_resolved_futures clearing the `pending_states`.
1094+
network_graph.pending_checks.check_resolved_futures(&network_graph);
10861095
assert!(!network_graph.pending_checks.too_many_checks_pending());
10871096
}
10881097
}

0 commit comments

Comments
 (0)