Skip to content

Commit 7c871f2

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 converting the held channels field in `PendingChecksContext` to hold an `Arc`, ensuring the data is still present even after the `UtxoFuture` is dropped. Signed-off-by: Elias Rohrer <dev@tnull.de>
1 parent af35837 commit 7c871f2

1 file changed

Lines changed: 59 additions & 59 deletions

File tree

lightning/src/routing/utxo.rs

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ impl UtxoFuture {
166166
}
167167

168168
struct PendingChecksContext {
169-
channels: HashMap<u64, Weak<Mutex<UtxoMessages>>>,
169+
channels: HashMap<u64, Arc<Mutex<UtxoMessages>>>,
170170
nodes: HashMap<NodeId, Vec<Weak<Mutex<UtxoMessages>>>>,
171171
}
172172

@@ -194,9 +194,13 @@ impl PendingChecks {
194194
) -> Result<(), LightningError> {
195195
let mut pending_checks = self.internal.lock().unwrap();
196196
if let hash_map::Entry::Occupied(e) = pending_checks.channels.entry(msg.short_channel_id) {
197-
let is_from_a = (msg.channel_flags & 1) == 1;
198-
match Weak::upgrade(e.get()) {
199-
Some(msgs_ref) => {
197+
if Arc::strong_count(e.get()) == 1 {
198+
// The future was dropped and the entry is the last reference held.
199+
e.remove();
200+
} else {
201+
let is_from_a = (msg.channel_flags & 1) == 1;
202+
{
203+
let msgs_ref = e.get();
200204
let mut messages = msgs_ref.lock().unwrap();
201205
let latest_update = if is_from_a {
202206
&mut messages.latest_channel_update_a
@@ -216,15 +220,12 @@ impl PendingChecks {
216220
ChannelUpdate::Unsigned(msg.clone())
217221
});
218222
}
219-
return Err(LightningError {
220-
err: "Awaiting channel_announcement validation to accept channel_update"
221-
.to_owned(),
222-
action: ErrorAction::IgnoreAndLog(Level::Gossip),
223-
});
224-
},
225-
None => {
226-
e.remove();
227-
},
223+
}
224+
return Err(LightningError {
225+
err: "Awaiting channel_announcement validation to accept channel_update"
226+
.to_owned(),
227+
action: ErrorAction::IgnoreAndLog(Level::Gossip),
228+
});
228229
}
229230
}
230231
Ok(())
@@ -284,22 +285,31 @@ impl PendingChecks {
284285

285286
fn check_replace_previous_entry(
286287
msg: &msgs::UnsignedChannelAnnouncement, full_msg: Option<&msgs::ChannelAnnouncement>,
287-
replacement: Option<Weak<Mutex<UtxoMessages>>>,
288-
pending_channels: &mut HashMap<u64, Weak<Mutex<UtxoMessages>>>,
288+
replacement: Option<Arc<Mutex<UtxoMessages>>>,
289+
pending_channels: &mut HashMap<u64, Arc<Mutex<UtxoMessages>>>,
289290
) -> Result<(), msgs::LightningError> {
290291
match pending_channels.entry(msg.short_channel_id) {
291292
hash_map::Entry::Occupied(mut e) => {
292-
// There's already a pending lookup for the given SCID. Check if the messages
293-
// are the same and, if so, return immediately (don't bother spawning another
294-
// lookup if we haven't gotten that far yet).
295-
match Weak::upgrade(&e.get()) {
296-
Some(pending_msgs) => {
297-
// This may be called with the mutex held on a different UtxoMessages
298-
// struct, however in that case we have a global lockorder of new messages
299-
// -> old messages, which makes this safe.
300-
let pending_matches = match &pending_msgs
301-
.unsafe_well_ordered_double_lock_self()
302-
.channel_announce
293+
if Arc::strong_count(e.get()) == 1 {
294+
// The future was dropped and the entry is the last reference held.
295+
//
296+
// The earlier lookup already resolved. We can't be sure its the same
297+
// so just remove/replace it and move on.
298+
if let Some(item) = replacement {
299+
*e.get_mut() = item;
300+
} else {
301+
e.remove();
302+
}
303+
} else {
304+
// There's already a pending lookup for the given SCID. Check if the messages
305+
// are the same and, if so, return immediately (don't bother spawning another
306+
// lookup if we haven't gotten that far yet).
307+
let pending_msgs = e.get();
308+
// This may be called with the mutex held on a different UtxoMessages
309+
// struct, however in that case we have a global lockorder of new messages
310+
// -> old messages, which makes this safe.
311+
let pending_matches =
312+
match &pending_msgs.unsafe_well_ordered_double_lock_self().channel_announce
303313
{
304314
Some(ChannelAnnouncement::Full(pending_msg)) => {
305315
Some(pending_msg) == full_msg
@@ -314,31 +324,21 @@ impl PendingChecks {
314324
false
315325
},
316326
};
317-
if pending_matches {
318-
return Err(LightningError {
319-
err: "Channel announcement is already being checked".to_owned(),
320-
action: ErrorAction::IgnoreDuplicateGossip,
321-
});
322-
} else {
323-
// The earlier lookup is a different message. If we have another
324-
// request in-flight now replace the original.
325-
// Note that in the replace case whether to replace is somewhat
326-
// arbitrary - both results will be handled, we're just updating the
327-
// value that will be compared to future lookups with the same SCID.
328-
if let Some(item) = replacement {
329-
*e.get_mut() = item;
330-
}
331-
}
332-
},
333-
None => {
334-
// The earlier lookup already resolved. We can't be sure its the same
335-
// so just remove/replace it and move on.
327+
if pending_matches {
328+
return Err(LightningError {
329+
err: "Channel announcement is already being checked".to_owned(),
330+
action: ErrorAction::IgnoreDuplicateGossip,
331+
});
332+
} else {
333+
// The earlier lookup is a different message. If we have another
334+
// request in-flight now replace the original.
335+
// Note that in the replace case whether to replace is somewhat
336+
// arbitrary - both results will be handled, we're just updating the
337+
// value that will be compared to future lookups with the same SCID.
336338
if let Some(item) = replacement {
337339
*e.get_mut() = item;
338-
} else {
339-
e.remove();
340340
}
341-
},
341+
}
342342
}
343343
},
344344
hash_map::Entry::Vacant(v) => {
@@ -416,7 +416,7 @@ impl PendingChecks {
416416
Self::check_replace_previous_entry(
417417
msg,
418418
full_msg,
419-
Some(Arc::downgrade(&future.state)),
419+
Some(Arc::clone(&future.state)),
420420
&mut pending_checks.channels,
421421
)?;
422422
async_messages.channel_announce = Some(if let Some(msg) = full_msg {
@@ -464,7 +464,7 @@ impl PendingChecks {
464464
// If we have many channel checks pending, ensure we don't have any dangling checks
465465
// (i.e. checks where the user told us they'd call back but drop'd the `UtxoFuture`
466466
// instead) before we commit to applying backpressure.
467-
pending_checks.channels.retain(|_, chan| Weak::upgrade(&chan).is_some());
467+
pending_checks.channels.retain(|_, chan| Arc::strong_count(chan) > 1);
468468
pending_checks.nodes.retain(|_, channels| {
469469
channels.retain(|chan| Weak::upgrade(&chan).is_some());
470470
!channels.is_empty()
@@ -575,16 +575,16 @@ impl PendingChecks {
575575
{
576576
let mut lck = self.internal.lock().unwrap();
577577
lck.channels.retain(|_, state| {
578-
if let Some(state) = state.upgrade() {
579-
if state.lock().unwrap().complete.is_some() {
580-
completed_states.push(state);
581-
false
582-
} else {
578+
if state.lock().unwrap().complete.is_some() {
579+
completed_states.push(Arc::clone(&state));
580+
false
581+
} else {
582+
if Arc::strong_count(&state) > 1 {
583583
true
584+
} else {
585+
// The UtxoFuture has been dropped, drop the pending-lookup state.
586+
false
584587
}
585-
} else {
586-
// The UtxoFuture has been dropped, drop the pending-lookup state.
587-
false
588588
}
589589
});
590590
lck.nodes.retain(|_, lookups| {
@@ -1142,7 +1142,7 @@ mod tests {
11421142
.unwrap_err();
11431143
assert!(network_graph.pending_checks.too_many_checks_pending());
11441144

1145-
// Once the future is drop'd (by resetting the `utxo_ret` value) the "too many checks" flag
1145+
// Once the future is drop'd (by resetting the `utxo_ret` value), the "too many checks" flag
11461146
// should reset to false.
11471147
*chain_source.utxo_ret.lock().unwrap() = UtxoResult::Sync(Err(UtxoLookupError::UnknownTx));
11481148
assert!(!network_graph.pending_checks.too_many_checks_pending());

0 commit comments

Comments
 (0)