From 1f1bccb674116084faa7c731232401f9262015bc Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 24 Aug 2026 21:58:04 +0200 Subject: [PATCH] [client] Serialize availability transitions in the netevents manager SetNetworkAvailable read IsOnline before updating netState without any locking, so concurrent availability flips could interleave: a false call could observe a stale offline state, skip MarkNetworkChange, and land after a racing true call, leaving the client offline with unswept connections. The netState and recorder updates could also apply out of order, letting the listener state disagree with netState. On Android the initial availability push at service start races the ConnectivityManager callbacks, and iOS feeds the same manager from NWPathMonitor. Hold a manager mutex across the check, sweep mark, state update and recorder update so transitions apply atomically and in order. --- client/netevents/netevents.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/netevents/netevents.go b/client/netevents/netevents.go index f5b00a67a..635539264 100644 --- a/client/netevents/netevents.go +++ b/client/netevents/netevents.go @@ -6,6 +6,7 @@ package netevents import ( "context" + "sync" "time" "github.com/cenkalti/backoff/v4" @@ -25,6 +26,10 @@ type Recorder interface { // the valid no-events value: the read methods report always-online and never // sweep. type Manager struct { + // mu serializes availability transitions: the IsOnline check and the + // state update must be atomic, or a racing offline flip can skip the sweep + // and leave netState and the recorder disagreeing. + mu sync.Mutex netState *netstate.State sweeper *sweep.Sweeper recorder Recorder @@ -48,6 +53,9 @@ func NewManager(recorder Recorder) *Manager { // "connected" until their own timeouts and the client would keep reporting // Connected with no network at all. func (m *Manager) SetNetworkAvailable(available bool) { + m.mu.Lock() + defer m.mu.Unlock() + if !available && m.netState.IsOnline() { m.sweeper.MarkNetworkChange() }