Bias `Selector` to first poll the sleeper future
What changed, and why it matters
This commit fixes a scheduling bug in a background task runner used by the Lightning Dev Kit. Previously, the runner could keep handling other background work forever and ignore a request to shut down, as long as other work kept appearing. Now it checks the shutdown timer first, so it stops promptly when asked. This is a reliability/availability fix rather than a direct theft-of-funds bug, but in a payment system, failing to shut down on time can contribute to operational problems or resource exhaustion.
Treat as a recommended reliability/availability patch. Node operators and downstream projects should update to ensure the background processor respects shutdown signals promptly. No immediate emergency response is indicated, but the fix should be included in the next maintenance release.
Security signals we found
Liveness issue: background processor could ignore shutdown signal under continuous ready futures
Priority inversion: lower-priority background work could starve the shutdown timer
Potential availability impact: delayed shutdown could affect node operation or restart behavior
No cryptographic, memory-safety, or authentication flaw evident in diff
Evidence from the diff
The Selector future in lightning-background-processor polled futures A-D before polling the sleeper future (E). Because A-D could be ready repeatedly, the loop could starve the sleeper and never observe the exit flag. The patch swaps the roles: the sleeper future is now A and is polled first, so exit is acted on immediately. The other futures are shifted to B-E. This is a priority-inversion / liveness fix.
Changed components
lightning-background-processor/src/lib.rsSelector futurebackground processor main loopInspect captured patch +19 / −17
diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs
index 19333c5..31e519b 100644
--- a/lightning-background-processor/src/lib.rs
+++ b/lightning-background-processor/src/lib.rs
@@ -476,11 +476,11 @@ pub(crate) mod futures_util {
use core::pin::Pin;
use core::task::{Poll, RawWaker, RawWakerVTable, Waker};
pub(crate) struct Selector<
- A: Future<Output = ()> + Unpin,
+ A: Future<Output = bool> + Unpin,
B: Future<Output = ()> + Unpin,
C: Future<Output = ()> + Unpin,
D: Future<Output = ()> + Unpin,
- E: Future<Output = bool> + Unpin,
+ E: Future<Output = ()> + Unpin,
> {
pub a: A,
pub b: B,
@@ -490,28 +490,30 @@ pub(crate) mod futures_util {
}
pub(crate) enum SelectorOutput {
- A,
+ A(bool),
B,
C,
D,
- E(bool),
+ E,
}
impl<
- A: Future<Output = ()> + Unpin,
+ A: Future<Output = bool> + Unpin,
B: Future<Output = ()> + Unpin,
C: Future<Output = ()> + Unpin,
D: Future<Output = ()> + Unpin,
- E: Future<Output = bool> + Unpin,
+ E: Future<Output = ()> + Unpin,
> Future for Selector<A, B, C, D, E>
{
type Output = SelectorOutput;
fn poll(
mut self: Pin<&mut Self>, ctx: &mut core::task::Context<'_>,
) -> Poll<SelectorOutput> {
+ // Bias the selector so it first polls the sleeper future, allowing to exit immediately
+ // if the flag is set.
match Pin::new(&mut self.a).poll(ctx) {
- Poll::Ready(()) => {
- return Poll::Ready(SelectorOutput::A);
+ Poll::Ready(res) => {
+ return Poll::Ready(SelectorOutput::A(res));
},
Poll::Pending => {},
}
@@ -534,8 +536,8 @@ pub(crate) mod futures_util {
Poll::Pending => {},
}
match Pin::new(&mut self.e).poll(ctx) {
- Poll::Ready(res) => {
- return Poll::Ready(SelectorOutput::E(res));
+ Poll::Ready(()) => {
+ return Poll::Ready(SelectorOutput::E);
},
Poll::Pending => {},
}
@@ -1037,15 +1039,15 @@ where
(false, false) => FASTEST_TIMER,
};
let fut = Selector {
- a: channel_manager.get_cm().get_event_or_persistence_needed_future(),
- b: chain_monitor.get_update_future(),
- c: om_fut,
- d: lm_fut,
- e: sleeper(sleep_delay),
+ a: sleeper(sleep_delay),
+ b: channel_manager.get_cm().get_event_or_persistence_needed_future(),
+ c: chain_monitor.get_update_future(),
+ d: om_fut,
+ e: lm_fut,
};
match fut.await {
- SelectorOutput::A | SelectorOutput::B | SelectorOutput::C | SelectorOutput::D => {},
- SelectorOutput::E(exit) => {
+ SelectorOutput::B | SelectorOutput::C | SelectorOutput::D | SelectorOutput::E => {},
+ SelectorOutput::A(exit) => {
if exit {
break;
}
Why this scored 46/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.