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

peer+lnwallet/chancloser: advance the legacy closer from one goroutine

Public commit record

What the developer wrote

Authored by Olaoluwa Osuntokun

88/100 · Strong
peer+lnwallet/chancloser: advance the legacy closer from one goroutine

In this commit, we give the legacy ChanCloser a single owner, rather than
letting two goroutines advance it. The peer's channelManager drives the state
machine for the Shutdown and ClosingSigned messages that come off the wire, and
for local close requests. The link drives it as well: while we wait for the
channel to drain we register a flush hook, and the link invokes that hook from
its own goroutine, where it called BeginNegotiation directly. Nothing kept the
two apart, so the state field, the priorFeeOffers map, and the signing step
could all be touched at once. Under `go test -race` this shows up as a data race
on the state field.

Rather than reach for a lock, we route the flush through the channelManager. The
hook now only reports the channel ID over a new chanCloseFlushed channel, and
handleChanFlushed picks it up next to the close messages. Every transition, the
cached offer processing, the fee map, and the signing then happen on the one
goroutine, so the closer needs no synchronization of its own. We spell that out
on the type, since it's an invariant a new caller can break from the outside.

The report goes out from a fresh goroutine, which matters more than it looks.
The link may well be holding its own lock while it invokes the hook, and
channelManager reaches for that same lock in DisableAdds, so blocking on the
handoff would trade the race for a deadlock. The `go` in front of RemoveLink
just above it is there for the same reason.

We look the closer up with a plain map load rather than through
fetchActiveChanCloser, as that one builds a fresh closer when it doesn't find
an existing one, and a flush that lands after the negotiation was torn down has
no business starting a new negotiation.

One behavior change falls out of the move: the flush path now runs the same
finalization tail as the message path. It skipped that before, so a responder
that drained a cached offer would reach closeFinished and broadcast, but nothing
ran finalizeChanClosure until the next close message showed up, and having
already sent its final signature, there may not be one. The link == nil path
already ran the tail, so this makes all three paths agree.

The new test drives a close with a link that hands us the flush hook instead of
running it inline, so we can check that negotiation waits on the report, and
that a report for a channel we have no closer for is dropped.
✓ Specific, descriptive subject✓ Names a concrete action or component✓ Provides detailed explanatory context✓ Mentions testing or verification✓ Names security-relevant behavior explicitly
The short version

What changed, and why it matters

This commit fixes a concurrency bug in how LND negotiates cooperative channel closures. Previously, two different goroutines could update the same channel-closing state machine at the same time, which could corrupt internal data or cause a crash. The fix routes all updates through a single goroutine and adds a test to confirm the behavior.

Recommended action

Reviewers should verify that all legacy ChanCloser transitions now occur only on the channelManager goroutine, that the non-blocking chanCloseFlushed handoff cannot leak goroutines on shutdown, and that the new test exercises both the wait-for-flush and stale-report-drop cases.

Security signals we found

01

Data race on ChanCloser state field detected under go test -race

02

Concurrent access to priorFeeOffers map and signing step by two goroutines

03

Single-goroutine ownership invariant now documented on ChanCloser type

04

Deadlock avoidance via non-blocking handoff from link goroutine

05

Behavior change: flush path now runs finalizeChanClosure consistently

Risk score

Why this scored 60/100

Our methodology →
Potential impact 18/30
Exploitability 8/25
Stealth signal 10/15
Affected reach 12/15
Confidence 8/10
Evidence quality 4/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.