-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
fix pinning in quic #52368
fix pinning in quic #52368
Changes from all commits
9241457
087fad4
7a4fb7d
e5c9877
86dd8cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,10 +51,9 @@ private sealed class State | |
|
||
// Buffers to hold during a call to send. | ||
public MemoryHandle[] BufferArrays = new MemoryHandle[1]; | ||
public QuicBuffer[] SendQuicBuffers = new QuicBuffer[1]; | ||
|
||
// Handle to pinned SendQuicBuffers. | ||
public GCHandle SendHandle; | ||
public IntPtr SendQuicBuffers; | ||
public int SendBufferMaxCount; | ||
public int SendBufferCount; | ||
|
||
// Resettable completions to be used for multiple calls to send, start, and shutdown. | ||
public readonly ResettableCompletionSource<uint> SendResettableCompletionSource = new ResettableCompletionSource<uint>(); | ||
|
@@ -176,14 +175,12 @@ internal override async ValueTask WriteAsync(ReadOnlyMemory<ReadOnlyMemory<byte> | |
|
||
using CancellationTokenRegistration registration = await HandleWriteStartState(cancellationToken).ConfigureAwait(false); | ||
await SendReadOnlyMemoryListAsync(buffers, endStream ? QUIC_SEND_FLAGS.FIN : QUIC_SEND_FLAGS.NONE).ConfigureAwait(false); | ||
|
||
HandleWriteCompletedState(); | ||
} | ||
|
||
internal override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, bool endStream, CancellationToken cancellationToken = default) | ||
{ | ||
ThrowIfDisposed(); | ||
|
||
using CancellationTokenRegistration registration = await HandleWriteStartState(cancellationToken).ConfigureAwait(false); | ||
|
||
await SendReadOnlyMemoryAsync(buffer, endStream ? QUIC_SEND_FLAGS.FIN : QUIC_SEND_FLAGS.NONE).ConfigureAwait(false); | ||
|
@@ -212,7 +209,7 @@ private async ValueTask<CancellationTokenRegistration> HandleWriteStartState(Can | |
bool shouldComplete = false; | ||
lock (state) | ||
{ | ||
if (state.SendState == SendState.None) | ||
if (state.SendState == SendState.None || state.SendState == SendState.Pending) | ||
{ | ||
state.SendState = SendState.Aborted; | ||
shouldComplete = true; | ||
|
@@ -240,7 +237,7 @@ private void HandleWriteCompletedState() | |
{ | ||
lock (_state) | ||
{ | ||
if (_state.SendState == SendState.Finished) | ||
if (_state.SendState == SendState.Finished || _state.SendState == SendState.Aborted) | ||
{ | ||
_state.SendState = SendState.None; | ||
} | ||
|
@@ -501,11 +498,11 @@ private void Dispose(bool disposing) | |
return; | ||
} | ||
|
||
_disposed = true; | ||
_state.Handle.Dispose(); | ||
Marshal.FreeHGlobal(_state.SendQuicBuffers); | ||
if (_stateHandle.IsAllocated) _stateHandle.Free(); | ||
CleanupSendState(_state); | ||
|
||
_disposed = true; | ||
} | ||
|
||
private void EnableReceive() | ||
|
@@ -602,7 +599,7 @@ private static uint HandleEventPeerRecvAborted(State state, ref StreamEvent evt) | |
bool shouldComplete = false; | ||
lock (state) | ||
{ | ||
if (state.SendState == SendState.None) | ||
if (state.SendState == SendState.None || state.SendState == SendState.Pending) | ||
{ | ||
shouldComplete = true; | ||
} | ||
|
@@ -761,7 +758,7 @@ private static uint HandleEventSendComplete(State state, ref StreamEvent evt) | |
|
||
lock (state) | ||
{ | ||
if (state.SendState == SendState.None) | ||
if (state.SendState == SendState.Pending) | ||
{ | ||
state.SendState = SendState.Finished; | ||
complete = true; | ||
|
@@ -771,7 +768,6 @@ private static uint HandleEventSendComplete(State state, ref StreamEvent evt) | |
if (complete) | ||
{ | ||
CleanupSendState(state); | ||
|
||
// TODO throw if a write was canceled. | ||
state.SendResettableCompletionSource.Complete(MsQuicStatusCodes.Success); | ||
} | ||
|
@@ -781,15 +777,15 @@ private static uint HandleEventSendComplete(State state, ref StreamEvent evt) | |
|
||
private static void CleanupSendState(State state) | ||
{ | ||
if (state.SendHandle.IsAllocated) | ||
lock (state) | ||
{ | ||
state.SendHandle.Free(); | ||
} | ||
Debug.Assert(state.SendState != SendState.Pending); | ||
Debug.Assert(state.SendBufferCount <= state.BufferArrays.Length); | ||
|
||
// Callings dispose twice on a memory handle should be okay | ||
foreach (MemoryHandle buffer in state.BufferArrays) | ||
{ | ||
buffer.Dispose(); | ||
for (int i = 0; i < state.SendBufferCount; i++) | ||
{ | ||
state.BufferArrays[i].Dispose(); | ||
} | ||
} | ||
} | ||
|
||
|
@@ -798,6 +794,12 @@ private unsafe ValueTask SendReadOnlyMemoryAsync( | |
ReadOnlyMemory<byte> buffer, | ||
QUIC_SEND_FLAGS flags) | ||
{ | ||
lock (_state) | ||
{ | ||
Debug.Assert(_state.SendState != SendState.Pending); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we want to prevent overlapping Sends. Shouldn't this then rather be a condition with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was not meant as guard agains overlaying Sends. (I think that should be done much earlier) |
||
_state.SendState = buffer.IsEmpty ? SendState.Finished : SendState.Pending; | ||
} | ||
|
||
if (buffer.IsEmpty) | ||
{ | ||
if ((flags & QUIC_SEND_FLAGS.FIN) == QUIC_SEND_FLAGS.FIN) | ||
|
@@ -809,18 +811,22 @@ private unsafe ValueTask SendReadOnlyMemoryAsync( | |
} | ||
|
||
MemoryHandle handle = buffer.Pin(); | ||
_state.SendQuicBuffers[0].Length = (uint)buffer.Length; | ||
_state.SendQuicBuffers[0].Buffer = (byte*)handle.Pointer; | ||
|
||
_state.BufferArrays[0] = handle; | ||
if (_state.SendQuicBuffers == IntPtr.Zero) | ||
{ | ||
_state.SendQuicBuffers = Marshal.AllocHGlobal(sizeof(QuicBuffer)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the major difference here is that we allocate What was wrong with the original approach? Was it This comment is just me trying to properly understand what's going on here. It has absolutely no influence on mergeability of this PR 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we could keep pinning. But then we do more operations on each send and we need to maintain the handle. So I felt this would be simpler as the SendQuicBuffers are really only consumed by native code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The general point here is that since we only use this array as unmanaged memory, there's not really any benefit to allocating it as managed memory, and some nontrivial cost (overhead of pin/unpin, additional GC overhead, etc). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But how does this solve the problem with byte mixing and AVE? Do we know the exact root cause of that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have doubts that this change is actually fixing the root cause of the crash. It is probably just moving it around. This type has number of subtle issues like #52048 that will lead to use-after-free and similar crashes. Somebody will need to do a focused pass over it to fix them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, you are right. Seems like this probably doesn't fix the root cause. It might make it less likely to happen, because the memory will never move; but I don't think we actually understand the root cause yet. (I had assumed we were previously allocating the array on the stack, per discussion above -- but we weren't, so that wasn't the root problem.) It would probably help to clear out the BufferArray entries when we dispose them, here: https://github.com/dotnet/runtime/pull/52368/files#diff-55ed6a1c110b1a90d4900bce3075ab49f1d6212c223e69b48f711f4084d264acR787 If we have a use-after-free issue, that would help find it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems suspicious that we are calling CleanupSendState in Dispose -- how do we know that msquic isn't still holding our buffer array? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree we need a pass here. Do you have specific concerns? Dispose in particular looks questionable to me, anything else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Handling of different failure modes. For example: If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to write new tests and cleanup the shutdown more. I would generally expect that if allocation fails, the stream (or whole system) will not be in usable state. I can certainly split the allocations and assignment to make sure it is consistent. |
||
_state.SendBufferMaxCount = 1; | ||
} | ||
|
||
_state.SendHandle = GCHandle.Alloc(_state.SendQuicBuffers, GCHandleType.Pinned); | ||
QuicBuffer* quicBuffers = (QuicBuffer*)_state.SendQuicBuffers; | ||
quicBuffers->Length = (uint)buffer.Length; | ||
quicBuffers->Buffer = (byte*)handle.Pointer; | ||
|
||
var quicBufferPointer = (QuicBuffer*)Marshal.UnsafeAddrOfPinnedArrayElement(_state.SendQuicBuffers, 0); | ||
_state.BufferArrays[0] = handle; | ||
_state.SendBufferCount = 1; | ||
|
||
uint status = MsQuicApi.Api.StreamSendDelegate( | ||
_state.Handle, | ||
quicBufferPointer, | ||
quicBuffers, | ||
bufferCount: 1, | ||
flags, | ||
IntPtr.Zero); | ||
|
@@ -841,6 +847,13 @@ private unsafe ValueTask SendReadOnlySequenceAsync( | |
ReadOnlySequence<byte> buffers, | ||
QUIC_SEND_FLAGS flags) | ||
{ | ||
|
||
lock (_state) | ||
{ | ||
Debug.Assert(_state.SendState != SendState.Pending); | ||
_state.SendState = buffers.IsEmpty ? SendState.Finished : SendState.Pending; | ||
} | ||
|
||
if (buffers.IsEmpty) | ||
{ | ||
if ((flags & QUIC_SEND_FLAGS.FIN) == QUIC_SEND_FLAGS.FIN) | ||
|
@@ -851,38 +864,39 @@ private unsafe ValueTask SendReadOnlySequenceAsync( | |
return default; | ||
} | ||
|
||
uint count = 0; | ||
int count = 0; | ||
|
||
foreach (ReadOnlyMemory<byte> buffer in buffers) | ||
{ | ||
++count; | ||
} | ||
|
||
if (_state.SendQuicBuffers.Length < count) | ||
if (_state.SendBufferMaxCount < count) | ||
{ | ||
_state.SendQuicBuffers = new QuicBuffer[count]; | ||
Marshal.FreeHGlobal(_state.SendQuicBuffers); | ||
jkotas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_state.SendQuicBuffers = IntPtr.Zero; | ||
_state.SendQuicBuffers = Marshal.AllocHGlobal(sizeof(QuicBuffer) * count); | ||
_state.SendBufferMaxCount = count; | ||
_state.BufferArrays = new MemoryHandle[count]; | ||
} | ||
|
||
_state.SendBufferCount = count; | ||
count = 0; | ||
|
||
QuicBuffer* quicBuffers = (QuicBuffer*)_state.SendQuicBuffers; | ||
foreach (ReadOnlyMemory<byte> buffer in buffers) | ||
{ | ||
MemoryHandle handle = buffer.Pin(); | ||
_state.SendQuicBuffers[count].Length = (uint)buffer.Length; | ||
_state.SendQuicBuffers[count].Buffer = (byte*)handle.Pointer; | ||
quicBuffers[count].Length = (uint)buffer.Length; | ||
quicBuffers[count].Buffer = (byte*)handle.Pointer; | ||
_state.BufferArrays[count] = handle; | ||
++count; | ||
} | ||
|
||
_state.SendHandle = GCHandle.Alloc(_state.SendQuicBuffers, GCHandleType.Pinned); | ||
|
||
var quicBufferPointer = (QuicBuffer*)Marshal.UnsafeAddrOfPinnedArrayElement(_state.SendQuicBuffers, 0); | ||
|
||
uint status = MsQuicApi.Api.StreamSendDelegate( | ||
_state.Handle, | ||
quicBufferPointer, | ||
count, | ||
quicBuffers, | ||
(uint)count, | ||
flags, | ||
IntPtr.Zero); | ||
|
||
|
@@ -902,6 +916,12 @@ private unsafe ValueTask SendReadOnlyMemoryListAsync( | |
ReadOnlyMemory<ReadOnlyMemory<byte>> buffers, | ||
QUIC_SEND_FLAGS flags) | ||
{ | ||
lock (_state) | ||
{ | ||
Debug.Assert(_state.SendState != SendState.Pending); | ||
_state.SendState = buffers.IsEmpty ? SendState.Finished : SendState.Pending; | ||
} | ||
|
||
if (buffers.IsEmpty) | ||
{ | ||
if ((flags & QUIC_SEND_FLAGS.FIN) == QUIC_SEND_FLAGS.FIN) | ||
|
@@ -916,28 +936,31 @@ private unsafe ValueTask SendReadOnlyMemoryListAsync( | |
|
||
uint length = (uint)array.Length; | ||
|
||
if (_state.SendQuicBuffers.Length < length) | ||
if (_state.SendBufferMaxCount < array.Length) | ||
{ | ||
_state.SendQuicBuffers = new QuicBuffer[length]; | ||
_state.BufferArrays = new MemoryHandle[length]; | ||
Marshal.FreeHGlobal(_state.SendQuicBuffers); | ||
jkotas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_state.SendQuicBuffers = IntPtr.Zero; | ||
_state.SendQuicBuffers = Marshal.AllocHGlobal(sizeof(QuicBuffer) * array.Length); | ||
_state.SendBufferMaxCount = array.Length; | ||
_state.BufferArrays = new MemoryHandle[array.Length]; | ||
} | ||
|
||
_state.SendBufferCount = array.Length; | ||
QuicBuffer* quicBuffers = (QuicBuffer*)_state.SendQuicBuffers; | ||
for (int i = 0; i < length; i++) | ||
{ | ||
ReadOnlyMemory<byte> buffer = array[i]; | ||
MemoryHandle handle = buffer.Pin(); | ||
_state.SendQuicBuffers[i].Length = (uint)buffer.Length; | ||
_state.SendQuicBuffers[i].Buffer = (byte*)handle.Pointer; | ||
_state.BufferArrays[i] = handle; | ||
} | ||
|
||
_state.SendHandle = GCHandle.Alloc(_state.SendQuicBuffers, GCHandleType.Pinned); | ||
quicBuffers[i].Length = (uint)buffer.Length; | ||
quicBuffers[i].Buffer = (byte*)handle.Pointer; | ||
|
||
var quicBufferPointer = (QuicBuffer*)Marshal.UnsafeAddrOfPinnedArrayElement(_state.SendQuicBuffers, 0); | ||
_state.BufferArrays[i] = handle; | ||
} | ||
|
||
uint status = MsQuicApi.Api.StreamSendDelegate( | ||
_state.Handle, | ||
quicBufferPointer, | ||
quicBuffers, | ||
length, | ||
flags, | ||
IntPtr.Zero); | ||
|
@@ -1014,6 +1037,7 @@ private enum ShutdownState | |
private enum SendState | ||
{ | ||
None, | ||
Pending, | ||
Aborted, | ||
Finished | ||
} | ||
|
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.
Why the lock here?
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 lock the
state
in other place when we transition. So I think we should be either consists or avoid the lock completely via some other mechanism (like preventing duplicate operations)