Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Fix lifetime handling of ReceiveMessageFromAsync buffer on Windows #22012

Merged
merged 3 commits into from
Jul 10, 2017

Conversation

stephentoub
Copy link
Member

When a ReceiveMessageFromAsync is first used with a SocketAsyncEventArgs instance, it initializes the _wsaMessageBuffer. Then in order to use the buffer, it pins it, storing both a GCHandle and a pointer to the target object. But if the SAEA's buffer is ever changed with, for example, a SetBuffer call, a routine is invoked on the SocketAsyncEventArgs that frees all of its pinned data, including these for the _wsaMessageBuffer. Then the next time ReceiveMessageFromAsync is used, this handle ends up not getting recreated, and we end up dereferencing a null pointer.

The essentially-one-line fix is to separate the creation of the buffer from the creation of the pinning handle, lazily initializing each independently. If the pinning handle is freed due to SetBuffer, the next invocation will reinitialize it.

I also consolidated and augmented the existing ReceiveMessageFromAsync tests to test multiple receives with the same SocketAsyncEventArgs in order to catch this case. The test now fails before the fix and passes after.

Contributes to https://github.com/dotnet/corefx/issues/21995 (hoping to port fix to 2.0 branch)
cc: @davidsh, @CIPop, @geoffkizer

The test currently only tests a single send/receive pair, which doesn't catch issues when trying to reuse a SocketAsyncEventArgs for multiple operations iteratively.  The test is also duplicated for IPv4 and IPv6.  Fix both issues.
When a ReceiveMessageFromAsync is first used with a SocketAsyncEventArgs instance, it initializes the _wsaMessageBuffer.  Then in order to use the buffer, it pins it, storing both a GCHandle and a pointer to the target object.  But if the SAEA's buffer is ever changed with, for example, a SetBuffer call, a routine is invoked on the SocketAsyncEventArgs that frees all of its pinned data, including these for the _wsaMessageBuffer.  Then the next time ReceiveMessageFromAsync is used, this handle ends up not getting recreated, and we end up dereferencing a null pointer.

The fix is to separate the creation of the buffer from the creation of the pinning handle, lazily initializing each independently.  If the pinning handle is freed due to SetBuffer, the next invocation will reinitialize it.
Debug.Assert(
!_wsaMessageBufferGCHandle.IsAllocated ||
(_wsaMessageBufferGCHandle.Target == _wsaMessageBuffer && _wsaMessageBuffer != null));
Debug.Assert((_ptrWSAMessageBuffer != IntPtr.Zero) == _wsaMessageBufferGCHandle.IsAllocated);

Choose a reason for hiding this comment

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

Seems like there should also be an assert that if _ptrWSAMessageBuffer is not null, then it's == Marshal.UnsafeAddrOfPinnedArrayElement(_wsaMessageBuffer, 0). That's correct, right?

BTW, at some point it would be nice to clean some of this stuff up, the pinning logic is unnecessarily complicated. A couple thoughts:

(1) Marshal.UnsafeAddrOfPinnedArrayElement(_wsaMessageBuffer, 0) can't be that expensive; rather than storing the raw ptr, we should just do this on demand.
(2) Even better, we could just allocate the WSAMessageBuffer (and WSABuffer also) on the stack when we do the actual native socket call. Again, I really doubt that the cost of doing this would matter at all.

Thoughts?
 

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 correct, right?

Yes.

BTW, at some point it would be nice to clean some of this stuff up

Yes, I was thinking the same thing yesterday, in particular because it'd be nice to remove the extra IntPtr field(s) from SAEA. I'm not doing that in this PR, though.

we could just allocate the WSAMessageBuffer (and WSABuffer also) on the stack when we do the actual native socket call

For an async call?

if (_wsaMessageBuffer == null)
{
_wsaMessageBuffer = new byte[sizeof(Interop.Winsock.WSAMsg)];
}
if (_ptrWSAMessageBuffer == IntPtr.Zero)
{

Choose a reason for hiding this comment

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

Just for clarity, I think I'd structure the asserts like this -- let me know if I missed something here...

if (_wsaMessageBuffer == null)
{
    Debug.Assert(!_wsaMessageBufferGCHandle.IsAllocated);
    Debug.Assert(_ptrWSAMessageBuffer == IntPtr.Zero);

    _wsaMessageBuffer = new byte[sizeof(Interop.Winsock.WSAMsg)];
}
if (_ptrWSAMessageBuffer == IntPtr.Zero)
{
    Debug.Assert(!_wsaMessageBufferGCHandle.IsAllocated);

    _wsaMessageBufferGCHandle = GCHandle.Alloc(_wsaMessageBuffer, GCHandleType.Pinned);
    _ptrWSAMessageBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(_wsaMessageBuffer, 0);
}
else
{
    Debug.Assert(_wsaMessageBufferGCHandle.IsAllocated);
    Debug.Assert(_ptrWSAMessageBuffer == Marshal.UnsafeAddrOfPinnedArrayElement(_wsaMessageBuffer, 0);
}

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.

@geoffkizer
Copy link

Generally LGTM

Copy link
Contributor

@davidsh davidsh left a comment

Choose a reason for hiding this comment

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

LGTM

@stephentoub stephentoub merged commit 5b767b4 into dotnet:master Jul 10, 2017
@stephentoub stephentoub deleted the receivemessage_fix branch July 10, 2017 02:16
@karelz karelz modified the milestone: 2.1.0 Jul 14, 2017
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…e_fix

Fix lifetime handling of ReceiveMessageFromAsync buffer on Windows

Commit migrated from dotnet/corefx@5b767b4
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants