Skip to content

Commit 0f7fec0

Browse files
committed
Fix data race leading to a deadlock when opening QuicStream (dotnet#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 0fdb133 commit 0f7fec0

File tree

3 files changed

+55
-14
lines changed

3 files changed

+55
-14
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

+23-5
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ static async ValueTask<QuicConnection> StartConnectAsync(QuicClientConnectionOpt
9898
/// </summary>
9999
private int _disposed;
100100

101+
/// <summary>
102+
/// Completed when connection shutdown is initiated.
103+
/// </summary>
104+
private TaskCompletionSource _connectionCloseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
105+
101106
private readonly ValueTaskSource _connectedTcs = new ValueTaskSource();
102107
private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource();
103108

@@ -376,16 +381,25 @@ public async ValueTask<QuicStream> OpenOutboundStreamAsync(QuicStreamType type,
376381
stream = new QuicStream(_handle, type, _defaultStreamErrorCode);
377382
await stream.StartAsync(cancellationToken).ConfigureAwait(false);
378383
}
379-
catch
384+
catch (Exception ex)
380385
{
381386
if (stream is not null)
382387
{
383388
await stream.DisposeAsync().ConfigureAwait(false);
384389
}
390+
391+
// Propagate ODE if disposed in the meantime.
392+
ObjectDisposedException.ThrowIf(_disposed == 1, this);
393+
394+
// In case of an incoming race when the connection is closed by the peer just before we open the stream,
395+
// we receive QUIC_STATUS_ABORTED from MsQuic, but we don't know how the connection was closed. We throw
396+
// special exception and handle it here where we can determine the shutdown reason.
397+
bool connectionAbortedByPeer = ThrowHelper.IsConnectionAbortedWhenStartingStreamException(ex);
398+
385399
// Propagate connection error if present.
386-
if (_acceptQueue.Reader.Completion.IsFaulted)
400+
if (_connectionCloseTcs.Task.IsFaulted || connectionAbortedByPeer)
387401
{
388-
await _acceptQueue.Reader.Completion.ConfigureAwait(false);
402+
await _connectionCloseTcs.Task.ConfigureAwait(false);
389403
}
390404
throw;
391405
}
@@ -475,17 +489,21 @@ private unsafe int HandleEventShutdownInitiatedByTransport(ref SHUTDOWN_INITIATE
475489
{
476490
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetExceptionForMsQuicStatus(data.Status, (long)data.ErrorCode));
477491
_connectedTcs.TrySetException(exception);
492+
_connectionCloseTcs.TrySetException(exception);
478493
_acceptQueue.Writer.TryComplete(exception);
479494
return QUIC_STATUS_SUCCESS;
480495
}
481496
private unsafe int HandleEventShutdownInitiatedByPeer(ref SHUTDOWN_INITIATED_BY_PEER_DATA data)
482497
{
483-
_acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode)));
498+
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode));
499+
_connectionCloseTcs.TrySetException(exception);
500+
_acceptQueue.Writer.TryComplete(exception);
484501
return QUIC_STATUS_SUCCESS;
485502
}
486503
private unsafe int HandleEventShutdownComplete()
487504
{
488-
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetOperationAbortedException());
505+
Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed == 1 ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException());
506+
_connectionCloseTcs.TrySetException(exception);
489507
_acceptQueue.Writer.TryComplete(exception);
490508
_connectedTcs.TrySetException(exception);
491509
_shutdownTcs.TrySetResult();

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,18 @@ internal unsafe QuicStream(MsQuicContextSafeHandle connectionHandle, QuicStreamT
161161
try
162162
{
163163
QUIC_HANDLE* handle;
164-
ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.StreamOpen(
164+
int status = MsQuicApi.Api.StreamOpen(
165165
connectionHandle,
166166
type == QuicStreamType.Unidirectional ? QUIC_STREAM_OPEN_FLAGS.UNIDIRECTIONAL : QUIC_STREAM_OPEN_FLAGS.NONE,
167167
&NativeCallback,
168168
(void*)GCHandle.ToIntPtr(context),
169-
&handle),
170-
"StreamOpen failed");
169+
&handle);
170+
171+
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? ex, streamWasSuccessfullyStarted: false, message: "StreamOpen failed"))
172+
{
173+
throw ex;
174+
}
175+
171176
_handle = new MsQuicContextSafeHandle(handle, context, SafeHandleType.Stream, connectionHandle);
172177
}
173178
catch
@@ -241,7 +246,8 @@ internal ValueTask StartAsync(CancellationToken cancellationToken = default)
241246
int status = MsQuicApi.Api.StreamStart(
242247
_handle,
243248
QUIC_STREAM_START_FLAGS.SHUTDOWN_ON_FAIL | QUIC_STREAM_START_FLAGS.INDICATE_PEER_ACCEPT);
244-
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? exception))
249+
250+
if (ThrowHelper.TryGetStreamExceptionForMsQuicStatus(status, out Exception? exception, streamWasSuccessfullyStarted: false))
245251
{
246252
_startedTcs.TrySetException(exception);
247253
}

0 commit comments

Comments
 (0)