[QUIC] Fix rooted connection when hanshake failed (#87328)
authorMarie Píchová <11718369+ManickaP@users.noreply.github.com>
Wed, 14 Jun 2023 11:34:21 +0000 (13:34 +0200)
committerGitHub <noreply@github.com>
Wed, 14 Jun 2023 11:34:21 +0000 (13:34 +0200)
* Added tests

* Fixed logging and minor typos

* Fix

* Fix test

* "Added asserts checking GCHandle state"

* Exclude test from mono

* Removed unnecessary _disposed checks

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicExtensions.cs [new file with mode: 0644]
src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicHelpers.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs
src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs

diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicExtensions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicExtensions.cs
new file mode 100644 (file)
index 0000000..fbade29
--- /dev/null
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Quic;
+
+namespace Microsoft.Quic;
+
+internal unsafe partial struct QUIC_NEW_CONNECTION_INFO
+{
+    public override string ToString()
+        => $"{{ {nameof(QuicVersion)} = {QuicVersion}, {nameof(LocalAddress)} = {LocalAddress->ToIPEndPoint()}, {nameof(RemoteAddress)} = {RemoteAddress->ToIPEndPoint()} }}";
+}
+
+internal unsafe partial struct QUIC_LISTENER_EVENT
+{
+    public override string ToString()
+        => Type switch
+        {
+            QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION
+                => $"{{ {nameof(NEW_CONNECTION.Info)} = {{ {nameof(QUIC_NEW_CONNECTION_INFO.QuicVersion)} = {NEW_CONNECTION.Info->QuicVersion}, {nameof(QUIC_NEW_CONNECTION_INFO.LocalAddress)} = {NEW_CONNECTION.Info->LocalAddress->ToIPEndPoint()}, {nameof(QUIC_NEW_CONNECTION_INFO.RemoteAddress)} = {NEW_CONNECTION.Info->RemoteAddress->ToIPEndPoint()} }} }}",
+            _ => string.Empty
+        };
+}
+
+internal unsafe partial struct QUIC_CONNECTION_EVENT
+{
+    public override string ToString()
+        => Type switch
+        {
+            QUIC_CONNECTION_EVENT_TYPE.CONNECTED
+                => $"{{ {nameof(CONNECTED.SessionResumed)} = {CONNECTED.SessionResumed} }}",
+            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_INITIATED_BY_TRANSPORT
+                => $"{{ {nameof(SHUTDOWN_INITIATED_BY_TRANSPORT.Status)} = {SHUTDOWN_INITIATED_BY_TRANSPORT.Status}, {nameof(SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode)} = {SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode} }}",
+            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_INITIATED_BY_PEER
+                => $"{{ {nameof(SHUTDOWN_INITIATED_BY_PEER.ErrorCode)} = {SHUTDOWN_INITIATED_BY_PEER.ErrorCode} }}",
+            QUIC_CONNECTION_EVENT_TYPE.SHUTDOWN_COMPLETE
+                => $"{{ {nameof(SHUTDOWN_COMPLETE.HandshakeCompleted)} = {SHUTDOWN_COMPLETE.HandshakeCompleted}, {nameof(SHUTDOWN_COMPLETE.PeerAcknowledgedShutdown)} = {SHUTDOWN_COMPLETE.PeerAcknowledgedShutdown}, {nameof(SHUTDOWN_COMPLETE.AppCloseInProgress)} = {SHUTDOWN_COMPLETE.AppCloseInProgress} }}",
+            QUIC_CONNECTION_EVENT_TYPE.LOCAL_ADDRESS_CHANGED
+                => $"{{ {nameof(LOCAL_ADDRESS_CHANGED.Address)} = {LOCAL_ADDRESS_CHANGED.Address->ToIPEndPoint()} }}",
+            QUIC_CONNECTION_EVENT_TYPE.PEER_ADDRESS_CHANGED
+                => $"{{ {nameof(PEER_ADDRESS_CHANGED.Address)} = {PEER_ADDRESS_CHANGED.Address->ToIPEndPoint()} }}",
+            QUIC_CONNECTION_EVENT_TYPE.PEER_STREAM_STARTED
+                => $"{{ {nameof(PEER_STREAM_STARTED.Flags)} = {PEER_STREAM_STARTED.Flags} }}",
+            QUIC_CONNECTION_EVENT_TYPE.PEER_CERTIFICATE_RECEIVED
+                => $"{{ {nameof(PEER_CERTIFICATE_RECEIVED.DeferredStatus)} = {PEER_CERTIFICATE_RECEIVED.DeferredStatus}, {nameof(PEER_CERTIFICATE_RECEIVED.DeferredErrorFlags)} = {PEER_CERTIFICATE_RECEIVED.DeferredErrorFlags} }}",
+            _ => string.Empty
+        };
+}
+
+internal unsafe partial struct QUIC_STREAM_EVENT
+{
+    public override string ToString()
+        => Type switch
+        {
+            QUIC_STREAM_EVENT_TYPE.START_COMPLETE
+                => $"{{ {nameof(START_COMPLETE.Status)} = {START_COMPLETE.Status}, {nameof(START_COMPLETE.ID)} = {START_COMPLETE.ID}, {nameof(START_COMPLETE.PeerAccepted)} = {START_COMPLETE.PeerAccepted} }}",
+            QUIC_STREAM_EVENT_TYPE.RECEIVE
+                => $"{{ {nameof(RECEIVE.AbsoluteOffset)} = {RECEIVE.AbsoluteOffset}, {nameof(RECEIVE.TotalBufferLength)} = {RECEIVE.TotalBufferLength}, {nameof(RECEIVE.Flags)} = {RECEIVE.Flags} }}",
+            QUIC_STREAM_EVENT_TYPE.SEND_COMPLETE
+                => $"{{ {nameof(SEND_COMPLETE.Canceled)} = {SEND_COMPLETE.Canceled} }}",
+            QUIC_STREAM_EVENT_TYPE.PEER_SEND_ABORTED
+                => $"{{ {nameof(PEER_SEND_ABORTED.ErrorCode)} = {PEER_SEND_ABORTED.ErrorCode} }}",
+            QUIC_STREAM_EVENT_TYPE.PEER_RECEIVE_ABORTED
+                => $"{{ {nameof(PEER_RECEIVE_ABORTED.ErrorCode)} = {PEER_RECEIVE_ABORTED.ErrorCode} }}",
+            QUIC_STREAM_EVENT_TYPE.SEND_SHUTDOWN_COMPLETE
+                => $"{{ {nameof(SEND_SHUTDOWN_COMPLETE.Graceful)} = {SEND_SHUTDOWN_COMPLETE.Graceful} }}",
+            QUIC_STREAM_EVENT_TYPE.SHUTDOWN_COMPLETE
+                => $"{{ {nameof(SHUTDOWN_COMPLETE.ConnectionShutdown)} = {SHUTDOWN_COMPLETE.ConnectionShutdown}, {nameof(SHUTDOWN_COMPLETE.ConnectionShutdownByApp)} = {SHUTDOWN_COMPLETE.ConnectionShutdownByApp}, {nameof(SHUTDOWN_COMPLETE.ConnectionClosedRemotely)} = {SHUTDOWN_COMPLETE.ConnectionClosedRemotely}, {nameof(SHUTDOWN_COMPLETE.ConnectionErrorCode)} = {SHUTDOWN_COMPLETE.ConnectionErrorCode}, {nameof(SHUTDOWN_COMPLETE.ConnectionCloseStatus)} = {SHUTDOWN_COMPLETE.ConnectionCloseStatus} }}",
+            _ => string.Empty
+        };
+}
index 683e8bb..bf454c0 100644 (file)
@@ -42,13 +42,13 @@ internal static class MsQuicHelpers
         return new Internals.SocketAddress(addressFamilyOverride ?? SocketAddressPal.GetAddressFamily(addressBytes), addressBytes).GetIPEndPoint();
     }
 
-    internal static unsafe QuicAddr ToQuicAddr(this IPEndPoint iPEndPoint)
+    internal static unsafe QuicAddr ToQuicAddr(this IPEndPoint ipEndPoint)
     {
         // TODO: is the layout same for SocketAddress.Buffer and QuicAddr on all platforms?
         QuicAddr result = default;
         Span<byte> rawAddress = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref result, 1));
 
-        Internals.SocketAddress address = IPEndPointExtensions.Serialize(iPEndPoint);
+        Internals.SocketAddress address = IPEndPointExtensions.Serialize(ipEndPoint);
         Debug.Assert(address.Size <= rawAddress.Length);
 
         address.Buffer.AsSpan(0, address.Size).CopyTo(rawAddress);
index 76dd9ea..e7c0cf8 100644 (file)
@@ -56,6 +56,18 @@ internal sealed class ResettableValueTaskSource : IValueTaskSource
     /// </summary>
     public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As<State, byte>(ref _state)) == State.Completed;
 
+    // TODO: Revisit this with https://github.com/dotnet/runtime/issues/79818 and https://github.com/dotnet/runtime/issues/79911
+    public bool KeepAliveReleased
+    {
+        get
+        {
+            lock (this)
+            {
+                return !_keepAlive.IsAllocated;
+            }
+        }
+    }
+
     /// <summary>
     /// Tries to get a value task representing this task source. If this task source is <see cref="State.None"/>, it'll also transition it into <see cref="State.Awaiting"/> state.
     /// It prevents concurrent operations from being invoked since it'll return <c>false</c> if the task source was already in <see cref="State.Awaiting"/> state.
@@ -153,7 +165,7 @@ internal sealed class ResettableValueTaskSource : IValueTaskSource
                     // Unblock the current task source and in case of a final also the final task source.
                     if (exception is not null)
                     {
-                        // Set up the exception stack strace for the caller.
+                        // Set up the exception stack trace for the caller.
                         exception = exception.StackTrace is null ? ExceptionDispatchInfo.SetCurrentStackTrace(exception) : exception;
                         if (state == State.None ||
                             state == State.Awaiting)
index ccb0f93..4422277 100644 (file)
@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics;
 using System.Net.Security;
 using System.Net.Sockets;
 using System.Runtime.CompilerServices;
@@ -329,8 +330,6 @@ public sealed partial class QuicConnection : IAsyncDisposable
 
     internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string targetHost, CancellationToken cancellationToken = default)
     {
-        ObjectDisposedException.ThrowIf(_disposed == 1, this);
-
         if (_connectedTcs.TryInitialize(out ValueTask valueTask, this, cancellationToken))
         {
             _canAccept = options.MaxInboundBidirectionalStreams > 0 || options.MaxInboundUnidirectionalStreams > 0;
@@ -473,19 +472,13 @@ public sealed partial class QuicConnection : IAsyncDisposable
 
         if (NetEventSource.Log.IsEnabled())
         {
-            NetEventSource.Info(this, $"{this} Received event CONNECTED {LocalEndPoint} -> {RemoteEndPoint}");
+            NetEventSource.Info(this, $"{this} Connection connected {LocalEndPoint} -> {RemoteEndPoint} for {_negotiatedApplicationProtocol} protocol");
         }
-
         _connectedTcs.TrySetResult();
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventShutdownInitiatedByTransport(ref SHUTDOWN_INITIATED_BY_TRANSPORT_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event SHUTDOWN_INITIATED_BY_TRANSPORT with {nameof(data.Status)}={data.Status}");
-        }
-
         // TODO: we should propagate transport error code.
         // https://github.com/dotnet/runtime/issues/72666
         Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetExceptionForMsQuicStatus(data.Status));
@@ -495,52 +488,29 @@ public sealed partial class QuicConnection : IAsyncDisposable
     }
     private unsafe int HandleEventShutdownInitiatedByPeer(ref SHUTDOWN_INITIATED_BY_PEER_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event SHUTDOWN_INITIATED_BY_PEER_DATA with {nameof(data.ErrorCode)}={data.ErrorCode}");
-        }
-
         _acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetConnectionAbortedException((long)data.ErrorCode)));
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventShutdownComplete()
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event SHUTDOWN_INITIATED_BY_PEER_DATA");
-        }
-
-        _acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetOperationAbortedException()));
+        Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetOperationAbortedException());
+        _acceptQueue.Writer.TryComplete(exception);
+        _connectedTcs.TrySetException(exception);
         _shutdownTcs.TrySetResult();
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventLocalAddressChanged(ref LOCAL_ADDRESS_CHANGED_DATA data)
     {
         _localEndPoint = data.Address->ToIPEndPoint();
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event LOCAL_ADDRESS_CHANGED with {nameof(data.Address)}={_localEndPoint}");
-        }
-
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventPeerAddressChanged(ref PEER_ADDRESS_CHANGED_DATA data)
     {
         _remoteEndPoint = data.Address->ToIPEndPoint();
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event LOCAL_ADDRESS_CHANGED with {nameof(data.Address)}={_remoteEndPoint}");
-        }
-
         return QUIC_STATUS_SUCCESS;
     }
     private unsafe int HandleEventPeerStreamStarted(ref PEER_STREAM_STARTED_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_STREAM_STARTED");
-        }
-
         QuicStream stream = new QuicStream(_handle, data.Stream, data.Flags, _defaultStreamErrorCode);
         if (!_acceptQueue.Writer.TryWrite(stream))
         {
@@ -557,11 +527,6 @@ public sealed partial class QuicConnection : IAsyncDisposable
     }
     private unsafe int HandleEventPeerCertificateReceived(ref PEER_CERTIFICATE_RECEIVED_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_CERTIFICATE_RECEIVED_DATA");
-        }
-
         try
         {
             return _sslConnectionOptions.ValidateCertificate((QUIC_BUFFER*)data.Certificate, (QUIC_BUFFER*)data.Chain, out _remoteCertificate);
@@ -573,16 +538,6 @@ public sealed partial class QuicConnection : IAsyncDisposable
         }
     }
 
-    private int HandleConnectionEvent(QUIC_CONNECTION_EVENT_TYPE type)
-    {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event {type}");
-        }
-
-        return QUIC_STATUS_SUCCESS;
-    }
-
     private unsafe int HandleConnectionEvent(ref QUIC_CONNECTION_EVENT connectionEvent)
         => connectionEvent.Type switch
         {
@@ -594,7 +549,7 @@ public sealed partial class QuicConnection : IAsyncDisposable
             QUIC_CONNECTION_EVENT_TYPE.PEER_ADDRESS_CHANGED => HandleEventPeerAddressChanged(ref connectionEvent.PEER_ADDRESS_CHANGED),
             QUIC_CONNECTION_EVENT_TYPE.PEER_STREAM_STARTED => HandleEventPeerStreamStarted(ref connectionEvent.PEER_STREAM_STARTED),
             QUIC_CONNECTION_EVENT_TYPE.PEER_CERTIFICATE_RECEIVED => HandleEventPeerCertificateReceived(ref connectionEvent.PEER_CERTIFICATE_RECEIVED),
-            _ => HandleConnectionEvent(connectionEvent.Type),
+            _ => QUIC_STATUS_SUCCESS,
         };
 
 #pragma warning disable CS3016
@@ -616,6 +571,11 @@ public sealed partial class QuicConnection : IAsyncDisposable
 
         try
         {
+            // Process the event.
+            if (NetEventSource.Log.IsEnabled())
+            {
+                NetEventSource.Info(instance, $"{instance} Received event {connectionEvent->Type} {connectionEvent->ToString()}");
+            }
             return instance.HandleConnectionEvent(ref *connectionEvent);
         }
         catch (Exception ex)
@@ -654,6 +614,7 @@ public sealed partial class QuicConnection : IAsyncDisposable
 
         // Wait for SHUTDOWN_COMPLETE, the last event, so that all resources can be safely released.
         await valueTask.ConfigureAwait(false);
+        Debug.Assert(_connectedTcs.IsCompleted);
         _handle.Dispose();
 
         _configuration?.Dispose();
index 841ca9b..7b4c661 100644 (file)
@@ -333,7 +333,7 @@ public sealed partial class QuicListener : IAsyncDisposable
             // Process the event.
             if (NetEventSource.Log.IsEnabled())
             {
-                NetEventSource.Info(instance, $"{instance} Received event {listenerEvent->Type}");
+                NetEventSource.Info(instance, $"{instance} Received event {listenerEvent->Type} {listenerEvent->ToString()}");
             }
             return instance.HandleListenerEvent(ref *listenerEvent);
         }
index 701a1ed..ac45fbb 100644 (file)
@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics;
 using System.IO;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
@@ -10,13 +11,13 @@ using Microsoft.Quic;
 using static System.Net.Quic.MsQuicHelpers;
 using static Microsoft.Quic.MsQuic;
 
-using START_COMPLETE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._START_COMPLETE_e__Struct;
-using RECEIVE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._RECEIVE_e__Struct;
-using SEND_COMPLETE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SEND_COMPLETE_e__Struct;
-using PEER_SEND_ABORTED = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._PEER_SEND_ABORTED_e__Struct;
-using PEER_RECEIVE_ABORTED = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._PEER_RECEIVE_ABORTED_e__Struct;
-using SEND_SHUTDOWN_COMPLETE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SEND_SHUTDOWN_COMPLETE_e__Struct;
-using SHUTDOWN_COMPLETE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SHUTDOWN_COMPLETE_e__Struct;
+using START_COMPLETE_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._START_COMPLETE_e__Struct;
+using RECEIVE_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._RECEIVE_e__Struct;
+using SEND_COMPLETE_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SEND_COMPLETE_e__Struct;
+using PEER_SEND_ABORTED_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._PEER_SEND_ABORTED_e__Struct;
+using PEER_RECEIVE_ABORTED_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._PEER_RECEIVE_ABORTED_e__Struct;
+using SEND_SHUTDOWN_COMPLETE_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SEND_SHUTDOWN_COMPLETE_e__Struct;
+using SHUTDOWN_COMPLETE_DATA = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._SHUTDOWN_COMPLETE_e__Struct;
 
 namespace System.Net.Quic;
 
@@ -233,8 +234,6 @@ public sealed partial class QuicStream
     /// <returns>An asynchronous task that completes with the opened <see cref="QuicStream" />.</returns>
     internal ValueTask StartAsync(CancellationToken cancellationToken = default)
     {
-        ObjectDisposedException.ThrowIf(_disposed == 1, this);
-
         _startedTcs.TryInitialize(out ValueTask valueTask, this, cancellationToken);
         {
             unsafe
@@ -489,13 +488,8 @@ public sealed partial class QuicStream
         }
     }
 
-    private unsafe int HandleEventStartComplete(ref START_COMPLETE data)
+    private unsafe int HandleEventStartComplete(ref START_COMPLETE_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event START_COMPLETE with {nameof(data.ID)}={data.ID}, {nameof(data.Status)}={data.Status} and {nameof(data.PeerAccepted)}={data.PeerAccepted}");
-        }
-
         _id = unchecked((long)data.ID);
         if (StatusSucceeded(data.Status))
         {
@@ -515,18 +509,13 @@ public sealed partial class QuicStream
 
         return QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventReceive(ref RECEIVE data)
+    private unsafe int HandleEventReceive(ref RECEIVE_DATA data)
     {
         ulong totalCopied = (ulong)_receiveBuffers.CopyFrom(
             new ReadOnlySpan<QUIC_BUFFER>(data.Buffers, (int)data.BufferCount),
             (int)data.TotalBufferLength,
             data.Flags.HasFlag(QUIC_RECEIVE_FLAGS.FIN));
 
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event RECEIVE with {nameof(data.BufferCount)}={data.BufferCount}, {nameof(data.TotalBufferLength)}={data.TotalBufferLength} and {nameof(totalCopied)}={totalCopied}");
-        }
-
         if (totalCopied < data.TotalBufferLength)
         {
             Volatile.Write(ref _receivedNeedsEnable, 1);
@@ -537,13 +526,8 @@ public sealed partial class QuicStream
         data.TotalBufferLength = totalCopied;
         return (_receiveBuffers.HasCapacity() && Interlocked.CompareExchange(ref _receivedNeedsEnable, 0, 1) == 1) ? QUIC_STATUS_CONTINUE : QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventSendComplete(ref SEND_COMPLETE data)
+    private unsafe int HandleEventSendComplete(ref SEND_COMPLETE_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event SEND_COMPLETE with {nameof(data.Canceled)}={data.Canceled}");
-        }
-
         // In case of cancellation, the task from _sendTcs is finished before the aborting. It is technically possible for subsequent WriteAsync to grab the next task
         // from _sendTcs and start executing before SendComplete event occurs for the previous (canceled) write
         lock (_sendBuffersLock)
@@ -559,44 +543,24 @@ public sealed partial class QuicStream
     }
     private unsafe int HandleEventPeerSendShutdown()
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_SEND_SHUTDOWN.");
-        }
-
         // Same as RECEIVE with FIN flag. Remember that no more RECEIVE events will come.
         // Don't set the task to its final state yet, but wait for all the buffered data to get consumed first.
         _receiveBuffers.SetFinal();
         _receiveTcs.TrySetResult();
         return QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventPeerSendAborted(ref PEER_SEND_ABORTED data)
+    private unsafe int HandleEventPeerSendAborted(ref PEER_SEND_ABORTED_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_SEND_ABORTED with {nameof(data.ErrorCode)}={data.ErrorCode}");
-        }
-
         _receiveTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode), final: true);
         return QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventPeerReceiveAborted(ref PEER_RECEIVE_ABORTED data)
+    private unsafe int HandleEventPeerReceiveAborted(ref PEER_RECEIVE_ABORTED_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_RECEIVE_ABORTED with {nameof(data.ErrorCode)}={data.ErrorCode}");
-        }
-
         _sendTcs.TrySetException(ThrowHelper.GetStreamAbortedException((long)data.ErrorCode), final: true);
         return QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventSendShutdownComplete(ref SEND_SHUTDOWN_COMPLETE data)
+    private unsafe int HandleEventSendShutdownComplete(ref SEND_SHUTDOWN_COMPLETE_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_RECEIVE_ABORTED with {nameof(data.Graceful)}={data.Graceful}");
-        }
-
         if (data.Graceful != 0)
         {
             _sendTcs.TrySetResult(final: true);
@@ -604,13 +568,8 @@ public sealed partial class QuicStream
         // If Graceful == 0, we either aborted write, received PEER_RECEIVE_ABORTED or will receive SHUTDOWN_COMPLETE(ConnectionClose) later, all of which completes the _sendTcs.
         return QUIC_STATUS_SUCCESS;
     }
-    private unsafe int HandleEventShutdownComplete(ref SHUTDOWN_COMPLETE data)
+    private unsafe int HandleEventShutdownComplete(ref SHUTDOWN_COMPLETE_DATA data)
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event SHUTDOWN_COMPLETE with  {nameof(data.ConnectionShutdown)}={data.ConnectionShutdown}");
-        }
-
         if (data.ConnectionShutdown != 0)
         {
             bool shutdownByApp = data.ConnectionShutdownByApp != 0;
@@ -639,25 +598,10 @@ public sealed partial class QuicStream
     }
     private unsafe int HandleEventPeerAccepted()
     {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event PEER_ACCEPTED");
-        }
-
         _startedTcs.TrySetResult();
         return QUIC_STATUS_SUCCESS;
     }
 
-    private int HandleStreamEvent(QUIC_STREAM_EVENT_TYPE type)
-    {
-        if (NetEventSource.Log.IsEnabled())
-        {
-            NetEventSource.Info(this, $"{this} Received event {type}");
-        }
-
-        return QUIC_STATUS_SUCCESS;
-    }
-
     private unsafe int HandleStreamEvent(ref QUIC_STREAM_EVENT streamEvent)
         => streamEvent.Type switch
         {
@@ -670,7 +614,7 @@ public sealed partial class QuicStream
             QUIC_STREAM_EVENT_TYPE.SEND_SHUTDOWN_COMPLETE => HandleEventSendShutdownComplete(ref streamEvent.SEND_SHUTDOWN_COMPLETE),
             QUIC_STREAM_EVENT_TYPE.SHUTDOWN_COMPLETE => HandleEventShutdownComplete(ref streamEvent.SHUTDOWN_COMPLETE),
             QUIC_STREAM_EVENT_TYPE.PEER_ACCEPTED => HandleEventPeerAccepted(),
-            _ => HandleStreamEvent(streamEvent.Type)
+            _ => QUIC_STATUS_SUCCESS
         };
 
 #pragma warning disable CS3016
@@ -692,6 +636,11 @@ public sealed partial class QuicStream
 
         try
         {
+            // Process the event.
+            if (NetEventSource.Log.IsEnabled())
+            {
+                NetEventSource.Info(instance, $"{instance} Received event {streamEvent->Type} {streamEvent->ToString()}");
+            }
             return instance.HandleStreamEvent(ref *streamEvent);
         }
         catch (Exception ex)
@@ -745,6 +694,10 @@ public sealed partial class QuicStream
 
         // Wait for SHUTDOWN_COMPLETE, the last event, so that all resources can be safely released.
         await valueTask.ConfigureAwait(false);
+        Debug.Assert(_startedTcs.IsCompleted);
+        // TODO: Revisit this with https://github.com/dotnet/runtime/issues/79818 and https://github.com/dotnet/runtime/issues/79911
+        Debug.Assert(_receiveTcs.KeepAliveReleased);
+        Debug.Assert(_sendTcs.KeepAliveReleased);
         _handle.Dispose();
 
         lock (_sendBuffersLock)
index ae10560..1a15e3f 100644 (file)
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
 using Microsoft.DotNet.XUnitExtensions;
 using Xunit;
 using Xunit.Abstractions;
+using TestUtilities;
 
 namespace System.Net.Quic.Tests
 {
@@ -57,6 +58,125 @@ namespace System.Net.Quic.Tests
             _certificates = setup;
         }
 
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))]
+        public async Task QuicRootedObjectGetReleased()
+        {
+            async Task<(WeakReference<QuicListener>, WeakReference<QuicConnection>, WeakReference<QuicConnection>, WeakReference<QuicStream>, WeakReference<QuicStream>)> GetWeakReferencesAsync()
+            {
+                // Set up all objects, keep their weak reference.
+                QuicListener listener = await CreateQuicListener();
+                WeakReference<QuicListener> wrListener = new WeakReference<QuicListener>(listener);
+
+                var (clientConnection, serverConnection) = await CreateConnectedQuicConnection(listener);
+                WeakReference<QuicConnection> wrClientConnection = new WeakReference<QuicConnection>(clientConnection);
+                WeakReference<QuicConnection> wrServerConnection = new WeakReference<QuicConnection>(serverConnection);
+
+                QuicStream clientStream = await clientConnection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
+                await clientStream.WriteAsync(new byte[5], completeWrites: true);
+
+                QuicStream serverStream = await serverConnection.AcceptInboundStreamAsync();
+                await serverStream.WriteAsync(new byte[10], completeWrites: true);
+
+                WeakReference<QuicStream> wrClientStream = new WeakReference<QuicStream>(clientStream);
+                WeakReference<QuicStream> wrServerStream = new WeakReference<QuicStream>(serverStream);
+
+                while (!clientStream.ReadsClosed.IsCompleted)
+                {
+                    int bytes = await clientStream.ReadAsync(new byte[10]);
+                    if (bytes == 0)
+                    {
+                        break;
+                    }
+                }
+                while (!serverStream.ReadsClosed.IsCompleted)
+                {
+                    int bytes = await serverStream.ReadAsync(new byte[10]);
+                    if (bytes == 0)
+                    {
+                        break;
+                    }
+                }
+
+                // Dispose everything and check if all weak references are dead.
+                await clientStream.DisposeAsync();
+                await serverStream.DisposeAsync();
+                await clientConnection.DisposeAsync();
+                await serverConnection.DisposeAsync();
+                await listener.DisposeAsync();
+
+                return (wrListener, wrClientConnection, wrServerConnection, wrClientStream, wrServerStream);
+            }
+
+            var (wrListener, wrClientConnection, wrServerConnection, wrClientStream, wrServerStream) = await GetWeakReferencesAsync();
+
+            for (int i = 0; i < 20; ++i)
+            {
+                GC.Collect();
+                GC.WaitForPendingFinalizers();
+
+                await Task.Delay(100 * i);
+
+                if (TestWeakReferences())
+                {
+                    continue;
+                }
+                break;
+
+                bool TestWeakReferences()
+                    => wrListener.TryGetTarget(out _) ||
+                       wrClientConnection.TryGetTarget(out _) ||
+                       wrServerConnection.TryGetTarget(out _) ||
+                       wrClientStream.TryGetTarget(out _) ||
+                       wrServerStream.TryGetTarget(out _);
+            }
+
+            Assert.False(wrListener.TryGetTarget(out _));
+            Assert.False(wrClientConnection.TryGetTarget(out _));
+            Assert.False(wrServerConnection.TryGetTarget(out _));
+            Assert.False(wrClientStream.TryGetTarget(out _));
+            Assert.False(wrServerStream.TryGetTarget(out _));
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime))]
+        public async Task QuicRootedConnectionGetsReleased_ConnectFails()
+        {
+            WeakReference<QuicConnection> wrServerConnection = default;
+            // Set up all objects, keep their weak reference.
+            var listenerOptions = CreateQuicListenerOptions();
+            listenerOptions.ConnectionOptionsCallback = (connection, _, _) =>
+            {
+                wrServerConnection = new WeakReference<QuicConnection>(connection);
+                var serverConnectionOptions = CreateQuicServerOptions();
+                serverConnectionOptions.ServerAuthenticationOptions = new SslServerAuthenticationOptions();
+                return ValueTask.FromResult(serverConnectionOptions);
+            };
+            QuicListener listener = await CreateQuicListener(listenerOptions);
+
+            await Assert.ThrowsAsync<AuthenticationException>(async () => await CreateConnectedQuicConnection(listener));
+
+            // Dispose everything and check if all weak references are dead.
+            await listener.DisposeAsync();
+
+            for (int i = 0; i < 20; ++i)
+            {
+                GC.Collect();
+                GC.WaitForPendingFinalizers();
+
+                await Task.Delay(100 * i);
+
+                if (TestWeakReferences())
+                {
+                    continue;
+                }
+                bool TestWeakReferences()
+                    => wrServerConnection.TryGetTarget(out _);
+
+                break;
+            }
+
+            Assert.False(wrServerConnection.TryGetTarget(out _));
+        }
+
         [Fact]
         public async Task ConnectWithCertificateChain()
         {