diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs index f9bd724a72d33..547f91d3e9143 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -719,7 +719,7 @@ public async Task ResponseCancellation_BothCancellationTokenAndDispose_Success() Exception ex = await Assert.ThrowsAnyAsync(() => stream.ReadAsync(new byte[1024], cancellationToken: cts.Token).AsTask()); // exact exception depends on who won the race - if (ex is not OperationCanceledException and not ObjectDisposedException) + if (ex is not OperationCanceledException) { var ioe = Assert.IsType(ex); var hre = Assert.IsType(ioe.InnerException); diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs index 6b70cc9b3c3b7..1aa31bd9873ca 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs @@ -534,7 +534,11 @@ private static unsafe int CopyMsQuicBuffersToUserBuffer(ReadOnlySpan internal override void AbortRead(long errorCode) { - ThrowIfDisposed(); + if (_disposed == 1) + { + // Dispose called AbortRead already + return; + } bool shouldComplete = false; lock (_state) @@ -562,7 +566,13 @@ internal override void AbortRead(long errorCode) internal override void AbortWrite(long errorCode) { - ThrowIfDisposed(); + if (_disposed == 1) + { + // Dispose already triggered graceful shutdown + // It is unsafe to try to trigger abortive shutdown now, because final event arriving after Dispose releases SafeHandle + // so if it arrives after our check but before we call msquic, me might end up with access violation + return; + } lock (_state) { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index 6e5d7f7a4b386..098bd4d2af1d2 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -710,6 +710,65 @@ async Task ReadUntilAborted() } ); } + + [Fact] + public async Task AbortAfterDispose_ProperlyOpenedStream_Success() + { + byte[] buffer = new byte[1] { 42 }; + var sem = new SemaphoreSlim(0); + + await RunClientServer( + clientFunction: async connection => + { + QuicStream stream = connection.OpenBidirectionalStream(); + // Force stream to open on the wire + await stream.WriteAsync(buffer); + await sem.WaitAsync(); + + stream.Dispose(); + + // should not throw ODE on aborting + stream.AbortRead(1234); + stream.AbortWrite(5675); + }, + serverFunction: async connection => + { + await using QuicStream stream = await connection.AcceptStreamAsync(); + Assert.Equal(1, await stream.ReadAsync(buffer)); + sem.Release(); + + // client will abort both sides, so we will receive the final event + await stream.ShutdownCompleted(); + } + ); + } + + [Fact] + public async Task AbortAfterDispose_StreamCreationFlushedByDispose_Success() + { + await RunClientServer( + clientFunction: connection => + { + QuicStream stream = connection.OpenBidirectionalStream(); + + // dispose will flush stream creation on the wire + stream.Dispose(); + + // should not throw ODE on aborting + stream.AbortRead(1234); + stream.AbortWrite(5675); + + return Task.CompletedTask; + }, + serverFunction: async connection => + { + await using QuicStream stream = await connection.AcceptStreamAsync(); + + // client will abort both sides, so we will receive the final event + await stream.ShutdownCompleted(); + } + ); + } } public sealed class QuicStreamTests_MockProvider : QuicStreamTests