p2p/discover: restore nextTimeout update in UDPv4 resetTimeout loop (#34878)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

The refactor from `for el := plist.Front(); ...; el = el.Next()` to the
new `iterList` iterator in #34743 silently dropped two things needed by
resetTimeout:

1. `nextTimeout = el.Value.(*replyMatcher)` at the top of the loop. This
assignment is what gives `nextTimeout` its documented meaning ("head of
plist when timeout was last reset"), and what makes the early-return
optimization at the top of resetTimeout work. Without it, nextTimeout is
only ever written to nil, so `nextTimeout == plist.Front().Value` is
always false and the optimization is dead.

2. `nextTimeout.errc <- errClockWarp` in the clock-warp branch now reads
a stale or nil pointer. Prior to the refactor, the inner assignment kept
nextTimeout pointing at the current matcher so its errc was the right
channel to receive the errClockWarp signal. After the refactor, on first
entry into the clock-warp branch nextTimeout is nil, which panics the
UDPv4 loop goroutine with a nil pointer deref and takes discv4 down.

Re-assign `nextTimeout = p` at the head of the loop (restoring the
documented invariant) and send the clock-warp error on `p.errc` rather
than the now-stale `nextTimeout.errc`.

The clock-warp branch triggers only when the system clock jumps backward
after a deadline is assigned (deadline - time.Now() >= 2*respTimeout,
i.e. at least ~500ms backward jump), which is why this regression
slipped past CI - it is not exercised by any existing unit test, and
writing one would require plumbing a clock through the loop.
This commit is contained in:
rayoo 2026-05-05 21:28:28 +08:00 committed by GitHub
parent 5b837e5786
commit 60db25b070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -447,6 +447,7 @@ func (t *UDPv4) loop() {
// Start the timer so it fires when the next pending reply has expired. // Start the timer so it fires when the next pending reply has expired.
now := time.Now() now := time.Now()
for p, el := range iterList[*replyMatcher](plist) { for p, el := range iterList[*replyMatcher](plist) {
nextTimeout = p
if dist := p.deadline.Sub(now); dist < 2*respTimeout { if dist := p.deadline.Sub(now); dist < 2*respTimeout {
timeout.Reset(dist) timeout.Reset(dist)
return return
@ -454,7 +455,7 @@ func (t *UDPv4) loop() {
// Remove pending replies whose deadline is too far in the // Remove pending replies whose deadline is too far in the
// future. These can occur if the system clock jumped // future. These can occur if the system clock jumped
// backwards after the deadline was assigned. // backwards after the deadline was assigned.
nextTimeout.errc <- errClockWarp p.errc <- errClockWarp
plist.Remove(el) plist.Remove(el)
} }
nextTimeout = nil nextTimeout = nil