Skip to content

Commit 127844c

Browse files
authored
Fix data race leading to a deadlock when opening QuicStream (#101250)
* Fix data race leading to a deadlock. * Remove unwanted change * Code review feedback * Fix hang * Add assert * Fix potential crash * Code review feedback
1 parent c0e3e61 commit 127844c

File tree

3 files changed

+51
-13
lines changed

3 files changed

+51
-13
lines changed

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ThrowHelper.cs

+22-5
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,27 @@ internal static QuicException GetOperationAbortedException(string? message = nul
2727
return new QuicException(QuicError.OperationAborted, null, message ?? SR.net_quic_operationaborted);
2828
}
2929

30-
internal static bool TryGetStreamExceptionForMsQuicStatus(int status, [NotNullWhen(true)] out Exception? exception)
30+
internal static bool TryGetStreamExceptionForMsQuicStatus(int status, [NotNullWhen(true)] out Exception? exception, bool streamWasSuccessfullyStarted = true, string? message = null)
3131
{
3232
if (status == QUIC_STATUS_ABORTED)
3333
{
34-
// If status == QUIC_STATUS_ABORTED, we will receive an event later, which will complete the task source.
35-
exception = null;
36-
return false;
34+
// Connection has been closed by the peer (either at transport or application level),
35+
if (streamWasSuccessfullyStarted)
36+
{
37+
// we will receive an event later, which will complete the stream with concrete
38+
// information why the connection was aborted.
39+
exception = null;
40+
return false;
41+
}
42+
else
43+
{
44+
// we won't be receiving any event callback for shutdown on this stream, so we don't
45+
// necessarily know which error to report. So we throw an exception which we can distinguish
46+
// at the caller (ConnectionAborted normally has App error code) and throw the correct
47+
// exception from there.
48+
exception = new QuicException(QuicError.ConnectionAborted, null, "");
49+
return true;
50+
}
3751
}
3852
else if (status == QUIC_STATUS_INVALID_STATE)
3953
{
@@ -43,13 +57,16 @@ internal static bool TryGetStreamExceptionForMsQuicStatus(int status, [NotNullWh
4357
}
4458
else if (StatusFailed(status))
4559
{
46-
exception = GetExceptionForMsQuicStatus(status);
60+
exception = GetExceptionForMsQuicStatus(status, message: message);
4761
return true;
4862
}
4963
exception = null;
5064
return false;
5165
}
5266

67+
// see TryGetStreamExceptionForMsQuicStatus for explanation
68+
internal static bool IsConnectionAbortedWhenStartingStreamException(Exception ex) => ex is QuicException qe && qe.QuicError == QuicError.ConnectionAborted && qe.ApplicationErrorCode is null;
69+
5370
internal static Exception GetExceptionForMsQuicStatus(int status, long? errorCode = default, string? message = null)
5471
{
5572
Exception ex = GetExceptionInternal(status, errorCode, message);

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ static async ValueTask<QuicConnection> StartConnectAsync(QuicClientConnectionOpt
108108
/// </summary>
109109
private int _disposed;
110110

111+
/// <summary>
112+
/// Completed when connection shutdown is initiated.
113+
/// </summary>
114+
private TaskCompletionSource _connectionCloseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
115+
111116
private readonly ValueTaskSource _connectedTcs = new ValueTaskSource();
112117
private readonly ResettableValueTaskSource _shutdownTcs = new ResettableValueTaskSource()
113118
{
@@ -424,7 +429,7 @@ public async ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type,
424429

425430
await stream.StartAsync(cancellationToken).ConfigureAwait(false);
426431
}
427-
catch
432+
catch (Exception ex)
428433
{
429434
if (stream is not null)
430435
{
@@ -433,10 +438,16 @@ public async ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type,
433438

434439
// Propagate ODE if disposed in the meantime.
435440
ObjectDisposedException.ThrowIf(_disposed == 1, this);
441+
442+
// In case of an incoming race when the connection is closed by the peer just before we open the stream,
443+
// we receive QUIC_STATUS_ABORTED from MsQuic, but we don't know how the connection was closed. We throw
444+
// special exception and handle it here where we can determine the shutdown reason.
445+
bool connectionAbortedByPeer = ThrowHelper.IsConnectionAbortedWhenStartingStreamException(ex);
446+
436447
// Propagate connection error if present.
437-
if (_acceptQueue.Reader.Completion.IsFaulted)
448+
if (_connectionCloseTcs.Task.IsFaulted || connectionAbortedByPeer)
438449
{
439-
await _acceptQueue.Reader.Completion.ConfigureAwait(false);
450+
await _connectionCloseTcs.Task.ConfigureAwait(false);
440451
}
441452
throw;
442453
}
@@ -534,12 +545,15 @@ private unsafe int HandleEventShutdownInitiatedByTransport(ref SHUTDOWN_INITIATE
534545
{
535546
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetExceptionForMsQuicStatus(data.Status, (long)data.ErrorCode));
536547
_connectedTcs.TrySetException(exception);
548+
_connectionCloseTcs.TrySetException(exception);
537549
_acceptQueue.Writer.TryComplete(exception);
538550
return QUIC_STATUS_SUCCESS;
539551
}
540552
private unsafe int HandleEventShutdownInitiatedByPeer(ref SHUTDOWN_INITIATED_BY_PEER_DATA data)
541553
{
542-
_acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode)));
554+
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode));
555+
_connectionCloseTcs.TrySetException(exception);
556+
_acceptQueue.Writer.TryComplete(exception);
543557
return QUIC_STATUS_SUCCESS;
544558
}
545559
private unsafe int HandleEventShutdownComplete()
@@ -548,6 +562,7 @@ private unsafe int HandleEventShutdownComplete()
548562
_tlsSecret?.WriteSecret();
549563

550564
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed == 1 ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException());
565+
_connectionCloseTcs.TrySetException(exception);
551566
_acceptQueue.Writer.TryComplete(exception);
552567
_connectedTcs.TrySetException(exception);
553568
_shutdownTokenSource.Cancel();

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ internal unsafe QuicStream(MsQuicContextSafeHandle connectionHandle, QuicStreamT
162162
try
163163
{
164164
QUIC_HANDLE* handle;
165-
ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.StreamOpen(
165+
int status = MsQuicApi.Api.StreamOpen(
166166
connectionHandle,
167167
type == QuicStreamType.Unidirectional ? QUIC_STREAM_OPEN_FLAGS.UNIDIRECTIONAL : QUIC_STREAM_OPEN_FLAGS.NONE,
168168
&NativeCallback,
169169
(void*)GCHandle.ToIntPtr(context),
170-
&handle),
171-
"StreamOpen failed");
170+
&handle);
171+
172+
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? ex, streamWasSuccessfullyStarted: false, message: "StreamOpen failed"))
173+
{
174+
throw ex;
175+
}
176+
172177
_handle = new MsQuicContextSafeHandle(handle, context, SafeHandleType.Stream, connectionHandle);
173178
_handle.Disposable = _sendBuffers;
174179
}
@@ -245,7 +250,8 @@ internal ValueTask StartAsync(CancellationToken cancellationToken = default)
245250
int status = MsQuicApi.Api.StreamStart(
246251
_handle,
247252
QUIC_STREAM_START_FLAGS.SHUTDOWN_ON_FAIL | QUIC_STREAM_START_FLAGS.INDICATE_PEER_ACCEPT);
248-
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? exception))
253+
254+
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? exception, streamWasSuccessfullyStarted: false))
249255
{
250256
_startedTcs.TrySetException(exception);
251257
}

0 commit comments

Comments
 (0)