AI-generated analysisPublished automatically and not human-verified. Validated context appears in community notes below.
← Watch feed
High 70 Bitcoin

htlcswitch: fix hodlQueue deadlock by stopping htlcManager first

Public commit record

What the developer wrote

Authored by ziggie

73/100 · Adequate
htlcswitch: fix hodlQueue deadlock by stopping htlcManager first

The channelLink.Stop() teardown had an inverted ordering that could
cause a permanent deadlock of the invoice registry under concurrent
peer disconnect.

The previous order was:
1. HodlUnsubscribeAll -- removes subscriptions
2. hodlQueue.Stop() -- kills the queue's internal goroutine
3. cg.Quit() -- signals htlcManager to stop
4. cg.WgWait() -- waits for htlcManager to exit

The race window between steps 2 and 4 left htlcManager alive. A
RevokeAndAck arriving during that window could drive processRemoteAdds
→ processExitHop → NotifyExitHopHtlc, registering a new hodl
subscription backed by a dead hodlQueue (ChanIn() has no reader).

Any subsequent call to notifyHodlSubscribers (e.g. MPP auto-release
timer, expiry watcher, or explicit settle/cancel) would then block
indefinitely on the unbuffered ChanIn(), holding hodlSubscriptionsMux.
Concurrent NotifyExitHopHtlc calls waiting for that lock, plus callers
holding the invoice-level lock waiting for those, produce a full
deadlock of the invoice registry with no recovery path short of a
daemon restart.

The fix is to stop htlcManager before touching the hodl subscription
state. htlcManager is the sole caller of NotifyExitHopHtlc, so once
cg.WgWait() returns no new subscriptions can be registered, making
HodlUnsubscribeAll and hodlQueue.Stop() race-free.
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context
The short version

What changed, and why it matters

This commit fixes a bug in LND's payment channel shutdown code that could freeze the entire invoice handling system. The old shutdown order stopped a message queue before stopping the goroutine that uses it, creating a brief window where a new subscription could be attached to a dead queue. Once that happened, later invoice operations would block forever waiting for a queue that would never respond, deadlocking the invoice registry and requiring a full node restart to recover. The fix simply reverses the shutdown order so the worker goroutine exits before the queue is torn down.

Recommended action

Apply the patch. It is a minimal, correct shutdown-order fix with no API or protocol changes. Nodes running affected versions should upgrade to avoid invoice-registry deadlock under peer disconnect. No immediate mitigation is available beyond restart if the deadlock is observed.

Security signals we found

01

Deadlock in invoice registry leading to denial of service

02

Race condition during channel teardown on concurrent peer disconnect

03

Unbuffered channel send to stopped queue causing indefinite block

04

Lock-order inversion involving hodlSubscriptionsMux and invoice-level locks

05

Fix reorders shutdown sequence to close producer before consumer

Risk score

Why this scored 70/100

Our methodology →
Potential impact 22/30
Exploitability 12/25
Stealth signal 10/15
Affected reach 12/15
Confidence 9/10
Evidence quality 5/5
Human-validated context

Community notes

Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.

No validated notes yet.

The AI analysis stands alone for now. Submit a note if you can add evidence or important context.