Skip to content

Conversation

@sanych-sun
Copy link
Member

Fix for the corner case on net6 on MacOS, when disposal of socket while connecting makes Connect to complete without any errors, but the socket is being Disposed and any next operation will fail with ObjectDisposedException.
It might be related to this issue: dotnet/runtime#75889

@sanych-sun sanych-sun requested a review from BorisDog December 8, 2025 21:40
@sanych-sun sanych-sun requested a review from a team as a code owner December 8, 2025 21:40
Copilot AI review requested due to automatic review settings December 8, 2025 21:40
@sanych-sun sanych-sun added the chore Non–user-facing code changes (tests, build scripts, etc.). label Dec 8, 2025
}
}

static void EnsureConnected(Socket socket)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation of the method is inspired by the recommented by Microsoft way to check if the socket is alive:
https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.connected?view=net-10.0

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a fix for a corner case on .NET 6 macOS where socket disposal during connection can cause Connect to complete without errors, leaving the socket in a disposed state that causes subsequent operations to fail. The fix introduces verification that the socket is truly connected and prevents unnecessary disposal when the connection succeeds.

Key changes:

  • Added EnsureConnected method to verify socket connectivity after Connect completes
  • Introduced ConnectOperationState to track connection success and prevent disposal of successfully connected sockets
  • Refactored cancellation callback to check connection state before disposing socket

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 228 to 239
bool originalBlockingState = socket.Blocking;
socket.Blocking = false;

try
{
// Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal.
socket.Send(__ensureConnectedBuffer, 0, 0);
}
finally
{
// Restore original blocking state
socket.Blocking = originalBlockingState;
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The socket's blocking state is not restored if accessing socket.Blocking throws an ObjectDisposedException. If the socket is already disposed when entering EnsureConnected, the getter for originalBlockingState will throw, but the finally block won't execute to restore the state (though this is moot since the socket is disposed). More critically, if the socket gets disposed between line 229 and the try block, the finally block will attempt to set socket.Blocking on a disposed socket. Consider wrapping the entire method body in a try-catch to handle ObjectDisposedException gracefully.

Suggested change
bool originalBlockingState = socket.Blocking;
socket.Blocking = false;
try
{
// Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal.
socket.Send(__ensureConnectedBuffer, 0, 0);
}
finally
{
// Restore original blocking state
socket.Blocking = originalBlockingState;
try
{
bool originalBlockingState = socket.Blocking;
socket.Blocking = false;
try
{
// Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal.
socket.Send(__ensureConnectedBuffer, 0, 0);
}
finally
{
// Restore original blocking state
socket.Blocking = originalBlockingState;
}
}
catch (ObjectDisposedException)
{
// Socket is already disposed, nothing to do.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK for the method to throw ObjectDisposedExcpetion, we have try-catch in the outer code.

Comment on lines 233 to 234
// Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal.
socket.Send(__ensureConnectedBuffer, 0, 0);
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending 0 bytes may not reliably detect a disposed socket on all platforms. Consider documenting why this specific approach was chosen for the macOS .NET 6 issue, or explore alternative detection methods such as checking socket.Connected property or using socket.Poll() with a zero timeout. The current implementation's effectiveness should be validated across different platforms.

Suggested change
// Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal.
socket.Send(__ensureConnectedBuffer, 0, 0);
// Use Poll and Connected to ensure the socket is actually connected.
// This is a cross-platform way to check for a disposed or disconnected socket.
if (!socket.Connected || !socket.Poll(0, SelectMode.SelectWrite))
{
throw new SocketException((int)SocketError.NotConnected);
}

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree. Sending the 0-bytes is done as per Microsoft's suggestion. Also socket.Connected does not makes any checks, but returns results based on the last operation results. Problem with socket.Poll() - we need to provide a small-timeout, what value should we use?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's OK. If ensuring the socket is alive takes too long - we want to interrupt the operation by disposing the socket.

Comment on lines 173 to 181
using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(state =>
{
var operationState = (ConnectOperationState)state;
if (operationState.IsSucceeded)
{
return;
}
DisposeSocket(operationState.Socket);
}, callbackState);
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition between setting callbackState.IsSucceeded = true on line 199 and checking operationState.IsSucceeded on line 176. If cancellation occurs between EnsureConnected completing and IsSucceeded being set, the socket could be disposed even though the connection succeeded. Consider setting IsSucceeded = true before calling EnsureConnected, or use a lock/interlocked operation to ensure thread-safe state updates.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot be sure if the connection was really established until we done the aliveness check, so we cannot set the value before calling EnsureConnected.

@sanych-sun sanych-sun requested a review from Copilot December 9, 2025 00:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


static void EnsureConnected(Socket socket)
{
bool originalBlockingState = socket.Blocking;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: var

#else
socket.Connect(endPoint);
#endif
if (!callbackState.TryChangeStatusFromInProgress(OperationCallbackState<Socket>.OperationStatus.Done))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a unittest for this with mock socket?

Copy link
Contributor

@BorisDog BorisDog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@sanych-sun sanych-sun merged commit e590d18 into mongodb:main Dec 9, 2025
22 of 23 checks passed
@sanych-sun sanych-sun deleted the csharp5798 branch December 9, 2025 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Non–user-facing code changes (tests, build scripts, etc.).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants