Skip to content

Commit 0f8072f

Browse files
committed
Avoid re-locking reused UTXO futures
UtxoLookup implementations may cache and return the same async future for repeated requests for a short channel id. When a replacement channel announcement arrived for an in-flight lookup, the async path held the future state while comparing the existing pending entry, which could point to that same state. Drop the state guard before checking or replacing the pending entry so repeated lookups can update the pending announcement without re-entering the mutex. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe
1 parent c9260ee commit 0f8072f

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

lightning/src/routing/utxo.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,16 @@ impl PendingChecks {
439439
pending_states.push(Arc::clone(&future.state));
440440
}
441441

442+
// The pending entry for this SCID may point to this same future.
443+
// Drop the state lock before checking for a replacement.
444+
drop(async_messages);
442445
Self::check_replace_previous_entry(
443446
msg,
444447
full_msg,
445448
Some(Arc::downgrade(&future.state)),
446449
&mut pending_checks.channels,
447450
)?;
451+
let mut async_messages = future.state.lock().unwrap();
448452
async_messages.channel_announce = Some(if let Some(msg) = full_msg {
449453
ChannelAnnouncement::Full(msg.clone())
450454
} else {
@@ -1028,6 +1032,56 @@ mod tests {
10281032
assert!(!is_test_feature_set);
10291033
}
10301034

1035+
#[test]
1036+
fn test_no_deadlock_same_future_different_announcement() {
1037+
// A user's UtxoLookup may return the same UtxoFuture for repeated lookups for a
1038+
// given SCID. A different channel_announcement with that SCID should replace the
1039+
// pending message without re-locking the already-held future state.
1040+
let (valid_announcement, chain_source, network_graph, good_script, ..) = get_test_objects();
1041+
let scid = valid_announcement.contents.short_channel_id;
1042+
1043+
let notifier = Arc::new(Notifier::new());
1044+
let future = UtxoFuture::new(Arc::clone(&notifier));
1045+
*chain_source.utxo_ret.lock().unwrap() = UtxoResult::Async(future.clone());
1046+
1047+
assert_eq!(
1048+
network_graph
1049+
.update_channel_from_announcement(&valid_announcement, &Some(&chain_source))
1050+
.unwrap_err()
1051+
.err,
1052+
"Channel being checked async"
1053+
);
1054+
assert_eq!(chain_source.get_utxo_call_count.load(Ordering::Relaxed), 1);
1055+
1056+
let secp_ctx = Secp256k1::new();
1057+
let replacement_pk_1 = &SecretKey::from_slice(&[99; 32]).unwrap();
1058+
let replacement_pk_2 = &SecretKey::from_slice(&[98; 32]).unwrap();
1059+
let replacement_announcement = get_signed_channel_announcement(
1060+
|msg| msg.features.set_unknown_feature_optional(),
1061+
replacement_pk_1,
1062+
replacement_pk_2,
1063+
&secp_ctx,
1064+
);
1065+
assert_eq!(
1066+
network_graph
1067+
.update_channel_from_announcement(&replacement_announcement, &Some(&chain_source))
1068+
.unwrap_err()
1069+
.err,
1070+
"Channel being checked async"
1071+
);
1072+
assert_eq!(chain_source.get_utxo_call_count.load(Ordering::Relaxed), 2);
1073+
1074+
future
1075+
.resolve(Ok(TxOut { value: Amount::from_sat(1_000_000), script_pubkey: good_script }));
1076+
assert!(notifier.notify_pending());
1077+
network_graph.pending_checks.check_resolved_futures(&network_graph);
1078+
#[rustfmt::skip]
1079+
let is_replacement_feature_set =
1080+
network_graph.read_only().channels().get(&scid).unwrap().announcement_message
1081+
.as_ref().unwrap().contents.features.supports_unknown_test_feature();
1082+
assert!(is_replacement_feature_set);
1083+
}
1084+
10311085
#[test]
10321086
fn test_checks_backpressure() {
10331087
// Test that too_many_checks_pending returns true when there are many checks pending, and

0 commit comments

Comments
 (0)