Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socket: cancel on-going operations when Sockets with non-owning handle gets disposed. #72000

Merged
merged 10 commits into from
Aug 4, 2022

Conversation

tmds
Copy link
Member

@tmds tmds commented Jul 12, 2022

Fixes #70311.

@halter73 @wfurt @antonfirsov @stephentoub @dotnet/ncl ptal.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Jul 12, 2022
@ghost
Copy link

ghost commented Jul 12, 2022

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #70311.

@halter73 @wfurt @antonfirsov @stephentoub @dotnet/ncl ptal.

Author: tmds
Assignees: -
Labels:

area-System.Net.Sockets

Milestone: -

// If we don't own the handle, cancel them explicitly.
if (!OwnsHandle)
{
Interop.Kernel32.CancelIoEx(handle, null);
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're cancelling all operations that are performed against the handle here.
If there is another SafeSocketHandle for the same raw handle in this process, it may get its operations aborted by this.

For Unix we track the on-going operations performed against the SafeHandle in SocketAsyncContext so we're only canceling the operations that were performed against that handle.
On Unix we actually don't support different SafeSocketHandles to perform async operations on the same raw handle. SocketAsyncEngine holds a map from the raw handle to one SocketAsyncContext.

Copy link
Member

Choose a reason for hiding this comment

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

Can we imagine a sane user scenario where this could lead to an issue?

Copy link
Member Author

Choose a reason for hiding this comment

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

I can't.

@karelz karelz requested a review from antonfirsov July 18, 2022 07:59
Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

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

Mostly questions and minor remarks.

: base(ownsHandle)
: base(ownsHandle: true) // To support canceling on-going operations we need to detect
// there are no more on-going operations.
// For that we the base-SafeHandle needs to be owning even
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// For that we the base-SafeHandle needs to be owning even
// For that the base-SafeHandle needs to be owning even

// If we don't own the handle, cancel them explicitly.
if (!OwnsHandle)
{
Interop.Kernel32.CancelIoEx(handle, null);
Copy link
Member

Choose a reason for hiding this comment

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

Can we imagine a sane user scenario where this could lead to an issue?

Comment on lines +192 to +193
// Don't disconnect sockets we don't own.
if (!OwnsHandle)
Copy link
Member

@antonfirsov antonfirsov Jul 18, 2022

Choose a reason for hiding this comment

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

So if we don't own the handle we won't attempt to abort ongoing sync operations anymore on Socket / SafeSocketHandle close? I assume this is not an issue, because in the owning case we are doing it only to prevent an indefinitely blocking close() syscall (or a handle leak if we don't close)? Am I following this correctly?

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're not doing it because the function has side-effects which we don't want on a non-owning socket.

Comment on lines 156 to 157
SocketError errorCode = OwnsHandle ? DoCloseHandle(abortive) : SocketError.Success;
return ret = errorCode == SocketError.Success;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: It's a bit weird to create a fake error code here. Maybe we can rewrite this part in the following form instead:

Suggested change
SocketError errorCode = OwnsHandle ? DoCloseHandle(abortive) : SocketError.Success;
return ret = errorCode == SocketError.Success;
return !OwnsHandle || DoCloseHandle(abortive) == SocketError.Success;

@@ -3147,6 +3146,11 @@ protected virtual void Dispose(bool disposing)
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Calling _handle.Dispose()");
handle.Dispose();
}
else if (!handle.OwnsHandle)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Calling _handle.CloseAsIs()");
Copy link
Member

Choose a reason for hiding this comment

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

Nit:
It might be beneficial to distinguish the different cases of Calling _handle.CloseAsIs() lines better, at least by logging if it's an abortive close.

@@ -139,6 +149,8 @@ public async Task ConnectGetsCanceledByDispose(string addressString, bool useDns
await RetryHelper.ExecuteAsync(async () =>
{
var client = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using SafeSocketHandle? owner = ReplaceWithNonOwning(ref client, owning);
Copy link
Member

Choose a reason for hiding this comment

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

We do not keep the reference of the original Socket instance after this line. Couldn't this lead to an unexpected eager finalization of that instance while client and owner are still alive?

Otherwise it will be just an ignored failing close() syscall in the finalizer, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we're replacing client (when owning is true) the original Socket gets:

            socket.SafeHandle.SetHandleAsInvalid();

so it will no longer close the platform socket.

@@ -42,18 +42,19 @@ public sealed partial class SafeSocketHandle : SafeHandleMinusOneIsInvalid
/// <param name="preexistingHandle">Handle to wrap</param>
/// <param name="ownsHandle">Whether to control the handle lifetime</param>
public SafeSocketHandle(IntPtr preexistingHandle, bool ownsHandle)
: base(ownsHandle)
: base(ownsHandle: true) // To support canceling on-going operations we need to detect
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be wise to add tests verifying that a non-owned handle will not be closed by Dispose and finalization. (Couldn't find existing ones, maybe just missed them?)

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 makes sense. I'll add those.

@@ -3147,6 +3146,11 @@ protected virtual void Dispose(bool disposing)
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "Calling _handle.Dispose()");
handle.Dispose();
}
else if (!handle.OwnsHandle)
Copy link
Member

Choose a reason for hiding this comment

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

nit: I personally don't like the negated booleans. I would use if (handle.OwnsHandle)) for the original block and add CloseAsIs() as new else.

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM.
I was thinking if there is good way to verify that we leave socket AS_IS In spirit of #70311.
But I could not come up with only good example. Perhaps duplicate the handle and verify that the copy is still useable...?

@antonfirsov
Copy link
Member

@wfurt can you please take a look on my latest changes?

  • Addressed some nits: e529b9b
  • Added test Ctor_Dispose_HandleClosedIfOwnsHandle: eef6bd2

If there are no objections to those changes, I will merge.

@wfurt
Copy link
Member

wfurt commented Aug 4, 2022

test failures unrelated.

@wfurt wfurt merged commit f6dec65 into dotnet:main Aug 4, 2022
@tmds
Copy link
Member Author

tmds commented Aug 5, 2022

@antonfirsov, thanks for finishing this up!

@karelz karelz added this to the 7.0.0 milestone Aug 7, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Sep 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net.Sockets community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Potential breaking change in socket close behavior in net6 over net5 on Linux
4 participants