Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 4d6521c

Browse files
authored
Stop artificially rooting SocketAsyncEventArgs (#28767)
SocketAsyncEventArgs creates an overlapped object that references the SAEA. That overlapped object creates an async pinning handle in the runtime, which roots the SocketAsyncEventArgs. The cycle from SAEA->Overlapped->handle->SAEA involving the root means that a dropped SAEA that's not Dispose'd will end up leaking. This commit fixes that by adding a level of indirection between the handle and the SAEA. Rather than wrapping the SAEA directly, the handle is given an intermediate object that references the SAEA, and the SAEA then stores itself as a reference into that object only while an active operation is in progress. Once the operation completes, the reference in that object is nulled out, and the SAEA will no longer be kept alive by the pinning handle.
1 parent 586cffc commit 4d6521c

File tree

3 files changed

+73
-2
lines changed

3 files changed

+73
-2
lines changed

src/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.Windows.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Buffers;
66
using System.Diagnostics;
77
using System.IO;
8+
using System.Runtime.CompilerServices;
89
using System.Runtime.InteropServices;
910
using System.Threading;
1011

@@ -40,6 +41,7 @@ private enum SingleBufferHandleState : byte { None, InProcess, Set }
4041

4142
// Overlapped object related variables.
4243
private PreAllocatedOverlapped _preAllocatedOverlapped;
44+
private readonly StrongBox<SocketAsyncEventArgs> _strongThisRef = new StrongBox<SocketAsyncEventArgs>(); // state for _preAllocatedOverlapped; .Value set to this while operations in flight
4345

4446
private PinState _pinState;
4547
private enum PinState : byte { None = 0, MultipleBuffer, SendPackets }
@@ -53,7 +55,7 @@ private void InitializeInternals()
5355
try
5456
{
5557
if (suppressFlow) ExecutionContext.SuppressFlow();
56-
_preAllocatedOverlapped = new PreAllocatedOverlapped(s_completionPortCallback, this, null);
58+
_preAllocatedOverlapped = new PreAllocatedOverlapped(s_completionPortCallback, _strongThisRef, null);
5759
}
5860
finally
5961
{
@@ -92,6 +94,15 @@ private unsafe void FreeNativeOverlapped(NativeOverlapped* overlapped)
9294
_currentSocket.SafeHandle.IOCPBoundHandle.FreeNativeOverlapped(overlapped);
9395
}
9496

97+
partial void StartOperationCommonCore()
98+
{
99+
// Store the reference to this instance so that it's kept alive by the preallocated
100+
// overlapped during the asynchronous operation and so that it's available in the
101+
// I/O completion callback. Once the operation completes, we null this out so
102+
// that the SocketAsyncEventArgs instance isn't kept alive unnecessarily.
103+
_strongThisRef.Value = this;
104+
}
105+
95106
/// <summary>Handles the result of an IOCP operation.</summary>
96107
/// <param name="success">true if the operation completed synchronously and successfully; otherwise, false.</param>
97108
/// <param name="bytesTransferred">The number of bytes transferred, if the operation completed synchronously and successfully.</param>
@@ -1115,6 +1126,7 @@ private SocketError FinishOperationConnect()
11151126

11161127
private void CompleteCore()
11171128
{
1129+
_strongThisRef.Value = null; // null out this reference from the overlapped so this isn't kept alive artificially
11181130
if (_singleBufferHandleState != SingleBufferHandleState.None)
11191131
{
11201132
CompleteCoreSpin();
@@ -1173,7 +1185,10 @@ private void FinishOperationSendPackets()
11731185

11741186
private static readonly unsafe IOCompletionCallback s_completionPortCallback = delegate (uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
11751187
{
1176-
var saea = (SocketAsyncEventArgs)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
1188+
var saeaBox = (StrongBox<SocketAsyncEventArgs>)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
1189+
SocketAsyncEventArgs saea = saeaBox.Value;
1190+
Debug.Assert(saea != null);
1191+
11771192
if ((SocketError)errorCode == SocketError.Success)
11781193
{
11791194
saea.FreeNativeOverlapped(nativeOverlapped);

src/System.Net.Sockets/src/System/Net/Sockets/SocketAsyncEventArgs.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,12 @@ internal void StartOperationCommon(Socket socket, SocketAsyncOperation operation
536536
{
537537
_context = ExecutionContext.Capture();
538538
}
539+
540+
StartOperationCommonCore();
539541
}
540542

543+
partial void StartOperationCommonCore();
544+
541545
internal void StartOperationAccept()
542546
{
543547
// AcceptEx needs a single buffer that's the size of two native sockaddr buffers with 16

src/System.Net.Sockets/tests/FunctionalTests/SocketAsyncEventArgsTest.netcoreapp.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
79
using System.Runtime.InteropServices;
10+
using System.Threading;
811
using Xunit;
912

1013
namespace System.Net.Sockets.Tests
@@ -192,5 +195,54 @@ public void SetBufferMemory_NonArray_BufferReturnsNull()
192195
Assert.Null(saea.Buffer);
193196
}
194197
}
198+
199+
[OuterLoop("Involves GC and finalization")]
200+
[Theory]
201+
[InlineData(false)]
202+
[InlineData(true)]
203+
public void Finalizer_InvokedWhenNoLongerReferenced(bool afterAsyncOperation)
204+
{
205+
var cwt = new ConditionalWeakTable<object, object>();
206+
207+
for (int i = 0; i < 5; i++) // create several SAEA instances, stored into cwt
208+
{
209+
CreateSocketAsyncEventArgs();
210+
211+
void CreateSocketAsyncEventArgs() // separated out so that JIT doesn't extend lifetime of SAEA instances
212+
{
213+
var saea = new SocketAsyncEventArgs();
214+
cwt.Add(saea, saea);
215+
216+
if (afterAsyncOperation)
217+
{
218+
using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
219+
{
220+
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
221+
listener.Listen(1);
222+
223+
using (Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
224+
{
225+
saea.RemoteEndPoint = listener.LocalEndPoint;
226+
using (var mres = new ManualResetEventSlim())
227+
{
228+
saea.Completed += (s, e) => mres.Set();
229+
if (client.ConnectAsync(saea))
230+
{
231+
mres.Wait();
232+
}
233+
}
234+
}
235+
}
236+
}
237+
}
238+
}
239+
240+
Assert.True(SpinWait.SpinUntil(() =>
241+
{
242+
GC.Collect();
243+
GC.WaitForPendingFinalizers();
244+
return cwt.Count() == 0; // validate that the cwt becomes empty
245+
}, 30_000));
246+
}
195247
}
196248
}

0 commit comments

Comments
 (0)