@@ -252,6 +252,15 @@ public void Stop()
252252 m_stopping = true ;
253253 }
254254
255+ /// <summary>
256+ /// Maximum timeout (in microseconds) passed to Socket.Select.
257+ /// On macOS ARM64, <c>Socket.Select</c> can fail to wake up when a TCP
258+ /// loopback socket becomes readable. Capping the timeout ensures the loop
259+ /// re-evaluates <see cref="m_stopping"/> periodically so that
260+ /// <see cref="Stop"/> requests are never missed.
261+ /// </summary>
262+ private const int MaxSelectTimeoutMicroseconds = 500_000 ; // 500,000 µs = 500 ms
263+
255264 /// <summary>
256265 /// This method is the polling-loop that is invoked on a background thread when Start is called.
257266 /// As long as Stop hasn't been called: execute the timers, and invoke the handler-methods on each of the saved PollSets.
@@ -274,11 +283,14 @@ private void Loop()
274283 try
275284 {
276285 timeout = timeout != 0 ? timeout * 1000 : - 1 ;
277- // Pass null for the error list: every socket is already tracked in
278- // m_checkRead (callers always invoke SetPollIn after AddHandle), so
279- // socket errors surface as readable events too. Passing a non-null
280- // error list alongside readList causes Socket.Select to hang
281- // indefinitely on macOS .NET (https://github.com/dotnet/corefx/issues/39617).
286+
287+ // Cap the timeout so the loop wakes up periodically. This
288+ // prevents an indefinite hang on platforms where Socket.Select
289+ // does not reliably detect readability on TCP loopback pairs
290+ // (the Signaler mechanism used by the Mailbox).
291+ if ( timeout < 0 || timeout > MaxSelectTimeoutMicroseconds )
292+ timeout = MaxSelectTimeoutMicroseconds ;
293+
282294 Socket . Select ( readList , null , null , timeout ) ;
283295 }
284296 catch ( SocketException )
0 commit comments