From 614971a90ff072d0b180e44ac348cf56b6443389 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 6 Jul 2026 13:50:43 -0400 Subject: [PATCH 1/2] slow-jam: always untrack a held payment when the interceptor exits The jamming interceptor only removed the payment from `jamming_payments` on the sleep (hold-time-elapsed) arm of the select. On the shutdown arm it returned without removing, leaving the entry behind. `run_attack`'s teardown loop waits for that set to drain, so a shutdown that landed while a payment was held would leave the loop spinning on a set that could never empty. Move the removal after the select so both the hold-time and shutdown paths untrack the payment. Co-Authored-By: Claude Opus 4.8 (1M context) --- ln-simln-jamming/src/attacks/slow_jam.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ln-simln-jamming/src/attacks/slow_jam.rs b/ln-simln-jamming/src/attacks/slow_jam.rs index a7e36751..613de2f8 100644 --- a/ln-simln-jamming/src/attacks/slow_jam.rs +++ b/ln-simln-jamming/src/attacks/slow_jam.rs @@ -300,16 +300,20 @@ where self.payment_trigger.0.trigger(); - // If this is one of our jamming payments, hold it - select! { - _ = req.shutdown_listener.clone() => Ok(Err(ForwardingError::InterceptorError("shutdown signal received".to_string()))), - _ = self.clock.sleep(hold_time) => { - self.jamming_payments.lock().await.remove(&req.payment_hash); - Ok(Err(ForwardingError::InterceptorError( - "failing from jamming interceptor".into(), - ))) - } - } + // Hold this jamming payment until the hold time elapses or we shut down. Either way, + // drop it from the tracked set afterwards so `run_attack`'s wait loop can terminate: + // leaving a shutdown-cancelled payment behind would make that loop spin forever. + let result = select! { + _ = req.shutdown_listener.clone() => Ok(Err(ForwardingError::InterceptorError( + "shutdown signal received".to_string(), + ))), + _ = self.clock.sleep(hold_time) => Ok(Err(ForwardingError::InterceptorError( + "failing from jamming interceptor".into(), + ))), + }; + + self.jamming_payments.lock().await.remove(&req.payment_hash); + result } } From d22f03b7a7d0739e265f80c7717832e290f84c04 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 6 Jul 2026 13:51:13 -0400 Subject: [PATCH 2/2] slow-jam: don't hold the lock across the teardown sleep; honor shutdown The teardown loop bound the `jamming_payments` guard to the whole loop body, so it held the mutex across the 5-minute `clock.sleep`. It also had no shutdown exit, relying solely on the set draining. Scope the lock to just the `is_empty` check so the guard drops before the sleep, and add a shutdown arm to the wait so the loop exits promptly on teardown instead of polling against a set that may not drain. Co-Authored-By: Claude Opus 4.8 (1M context) --- ln-simln-jamming/src/attacks/slow_jam.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ln-simln-jamming/src/attacks/slow_jam.rs b/ln-simln-jamming/src/attacks/slow_jam.rs index 613de2f8..144cd082 100644 --- a/ln-simln-jamming/src/attacks/slow_jam.rs +++ b/ln-simln-jamming/src/attacks/slow_jam.rs @@ -411,14 +411,18 @@ where // With protected resources jammed, check that test payment fails. check_payment(false).await?; - // Return when we are finished holding the payment. + // Wait until the held jamming payment resolves - its interceptor removes it from the set - + // or bail out early on shutdown. The lock is scoped to the emptiness check so the guard is + // never held across the sleep below. loop { - let jamming_payments_lock = self.jamming_payments.lock().await; - if jamming_payments_lock.is_empty() { + if self.jamming_payments.lock().await.is_empty() { break; } - self.clock.sleep(Duration::from_secs(60 * 5)).await; + select! { + _ = shutdown_listener.clone() => break, + _ = self.clock.sleep(Duration::from_secs(60 * 5)) => {}, + } } Ok(())