Skip to content

Guard discv4/discv5 protocol pointers on the generic node - #35

Merged
pk910 merged 1 commit into
ethpandaops:masterfrom
damilolaedwards:nodes-guard-protocol-pointers
Jul 29, 2026
Merged

Guard discv4/discv5 protocol pointers on the generic node#35
pk910 merged 1 commit into
ethpandaops:masterfrom
damilolaedwards:nodes-guard-protocol-pointers

Conversation

@damilolaedwards

Copy link
Copy Markdown
Contributor

Problem

The generic node in the nodes package published its v4Node and v5Node pointers with no synchronization. SetV4 and SetV5 wrote them lock free while V4, V5, HasV4, HasV5, PeerID, Enode, UpdateENR and CalculateScore read them lock free. These run on different goroutines in normal operation (protocol detection updates the pointers while a scoring sweep, a database batch or the web UI reads them), so the access is a data race. The check then deref readers are worse: SetV5(nil) can clear the pointer between the nil check and the call, so PeerID and CalculateScore can dereference nil and crash the daemon.

Fix

Guard both pointers with the node mutex that already protects addr. Simple accessors take the read lock. SetV4 and SetV5 take the write lock around the pointer store only. The check then deref readers capture the pointer under the read lock, release, then operate on the local copy, so a concurrent clear can no longer be observed mid call.

Tests

  • concurrent SetV4/SetV5 against every reader, which fails under the race detector if any pointer access is unsynchronized
  • a swap test that flips v5 support on and off while PeerID runs

Both pass under the race detector, alongside go build ./... and go vet.

Notes

  1. The fix reuses the existing node mutex rather than adding a dedicated lock. That mutex already guards addr and was already read for v4Node inside SetAddr, so reusing it keeps the locking model simple and consistent.
  2. The check then deref readers capture the pointer under the read lock and release before calling into the protocol node, so the lock is never held across an external call. This avoids reentrant read locking and keeps hold times short, at the cost of operating on a snapshot of the pointer, which is the intended behavior here.
  3. Scope: UpdateENR still writes the enr field lock free, and there is a separate unsynchronized stats pointer inside the discv5 node. Both are distinct from the protocol pointer race fixed here and are left for their own changes.

The generic node published its v4Node and v5Node pointers with no
synchronization. SetV4 and SetV5 wrote them lock free while V4, V5,
HasV4, HasV5, PeerID, Enode, UpdateENR and CalculateScore read them lock
free. These run on different goroutines in normal operation, for example
protocol detection updating the pointers while a scoring sweep, a
database batch or the web UI reads them, so the access is a data race.
The check then deref readers are worse: SetV5(nil) can clear the pointer
between the nil check and the call, so PeerID and CalculateScore can
dereference nil and crash the daemon.

Guard both pointers with the node mutex that already protects addr.
Simple accessors take the read lock. SetV4 and SetV5 take the write lock
around the pointer store only. The check then deref readers capture the
pointer under the read lock and release before the call, so a concurrent
clear can no longer be observed mid call.
@pk910
pk910 merged commit e666739 into ethpandaops:master Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants