bug: data race and leaked dialing counter in DialForOutboundPeers causes node to stop connecting new peers
Author: Sertug17Created Aug 4, 2026Updated Aug 4, 2026
Summary
DialForOutboundPeers() in p2p/p2p.go has two related bugs in the initial config peer dial loop:
- Data race the
dialingvariable is written from spawned goroutines and read from the main loop without any synchronization. - Counter never decremented goroutines increment
dialingbut never decrement it afterDialWithBackoffreturns, so the counter stays permanently inflated.
Affected Code
File: p2p/p2p.go → DialForOutboundPeers()
for _, peerString := range p.config.DialPeers {
peerAddress, err := getPeerFromString(peerString)
if err != nil {
continue
}
go func() {
dialing++ // ← unsynchronized write from goroutine
p.DialWithBackoff(peerAddress, true)
// ← dialing is NEVER decremented after DialWithBackoff returns
}()
}
// main loop reads dialing without synchronization:
if outbound > 0 && outbound+dialing >= p.config.MaxOutbound {
return // ← falsely triggered once initial goroutines finish
}Impact
- After the initial
DialPeersgoroutines complete,dialingstays permanently atlen(p.config.DialPeers). - The main dial loop evaluates
outbound + dialing >= MaxOutboundas true and stops dialing new peers, even when the node is well under its outbound peer limit. - Running with the
-raceflag will surface the data race ondialing.
Steps to Reproduce
- Set one or more entries in
DialPeersin node config - Start node with
go run -race ./cmd/... - Observe data race warning on
dialinginDialForOutboundPeers - After initial dial goroutines finish, observe node stops making new outbound connections despite being under
MaxOutbound
Suggested Fix
Use sync/atomic and add a deferred decrement:
var dialing atomic.Int64
for _, peerString := range p.config.DialPeers {
peerAddress, err := getPeerFromString(peerString)
if err != nil {
continue
}
go func() {
dialing.Add(1)
defer dialing.Add(-1)
p.DialWithBackoff(peerAddress, true)
}()
}
// main loop:
if outbound > 0 && int64(outbound)+dialing.Load() >= int64(p.config.MaxOutbound) {
return
}Source: canopy-network/canopy