-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Conversation
Tagging subscribers to this area: @dotnet/ncl Issue DetailsFixes #70311. @halter73 @wfurt @antonfirsov @stephentoub @dotnet/ncl ptal.
|
// If we don't own the handle, cancel them explicitly. | ||
if (!OwnsHandle) | ||
{ | ||
Interop.Kernel32.CancelIoEx(handle, null); |
There was a problem hiding this comment.
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 SafeSocketHandle
s to perform async operations on the same raw handle. SocketAsyncEngine
holds a map from the raw handle to one SocketAsyncContext
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't.
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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); |
There was a problem hiding this comment.
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?
// Don't disconnect sockets we don't own. | ||
if (!OwnsHandle) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
SocketError errorCode = OwnsHandle ? DoCloseHandle(abortive) : SocketError.Success; | ||
return ret = errorCode == SocketError.Success; |
There was a problem hiding this comment.
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:
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()"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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...?
This reverts commit 944910f.
test failures unrelated. |
@antonfirsov, thanks for finishing this up! |
Fixes #70311.
@halter73 @wfurt @antonfirsov @stephentoub @dotnet/ncl ptal.