Fix async handle leak when sync RandomAccess read/write on async handle fails (#69956)
authorStephen Toub <stoub@microsoft.com>
Fri, 10 Jun 2022 14:17:24 +0000 (10:17 -0400)
committerGitHub <noreply@github.com>
Fri, 10 Jun 2022 14:17:24 +0000 (10:17 -0400)
* Fix async handle leak when sync RandomAccess read/write on async handle fails

* Use UnsafeAllocateNativeOverlapped in GetNativeOverlappedForAsyncHandle

This isn't invoking user code and it's entirely for synchronous operations; there's no reason it needs to capture ExecutionContext.

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs

index 234782b..6a33837 100644 (file)
@@ -104,6 +104,14 @@ namespace System.IO
 
                         errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
                     }
+                    else
+                    {
+                        // The initial errorCode was neither ERROR_IO_PENDING nor ERROR_SUCCESS, so the operation
+                        // failed with an error and the callback won't be invoked.  We thus need to decrement the
+                        // ref count on the resetEvent that was initialized to a value under the expectation that
+                        // the callback would be invoked and decrement it.
+                        resetEvent.ReleaseRefCount(overlapped);
+                    }
 
                     switch (errorCode)
                     {
@@ -125,7 +133,7 @@ namespace System.IO
             {
                 if (overlapped != null)
                 {
-                    resetEvent.FreeNativeOverlapped(overlapped);
+                    resetEvent.ReleaseRefCount(overlapped);
                 }
 
                 resetEvent.Dispose();
@@ -203,6 +211,14 @@ namespace System.IO
 
                         errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
                     }
+                    else
+                    {
+                        // The initial errorCode was neither ERROR_IO_PENDING nor ERROR_SUCCESS, so the operation
+                        // failed with an error and the callback won't be invoked.  We thus need to decrement the
+                        // ref count on the resetEvent that was initialized to a value under the expectation that
+                        // the callback would be invoked and decrement it.
+                        resetEvent.ReleaseRefCount(overlapped);
+                    }
 
                     switch (errorCode)
                     {
@@ -225,7 +241,7 @@ namespace System.IO
             {
                 if (overlapped != null)
                 {
-                    resetEvent.FreeNativeOverlapped(overlapped);
+                    resetEvent.ReleaseRefCount(overlapped);
                 }
 
                 resetEvent.Dispose();
@@ -711,7 +727,7 @@ namespace System.IO
         {
             // After SafeFileHandle is bound to ThreadPool, we need to use ThreadPoolBinding
             // to allocate a native overlapped and provide a valid callback.
-            NativeOverlapped* result = handle.ThreadPoolBinding!.AllocateNativeOverlapped(s_callback, resetEvent, null);
+            NativeOverlapped* result = handle.ThreadPoolBinding!.UnsafeAllocateNativeOverlapped(s_callback, resetEvent, null);
 
             if (handle.CanSeek)
             {
@@ -751,7 +767,7 @@ namespace System.IO
             static unsafe void Callback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped)
             {
                 CallbackResetEvent state = (CallbackResetEvent)ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped)!;
-                state.FreeNativeOverlapped(pOverlapped);
+                state.ReleaseRefCount(pOverlapped);
             }
         }
 
@@ -767,7 +783,7 @@ namespace System.IO
         private static bool IsEndOfFileForNoBuffering(SafeFileHandle fileHandle, long fileOffset)
             => fileHandle.IsNoBuffering && fileHandle.CanSeek && fileOffset >= fileHandle.GetFileLength();
 
-        // We need to store the reference count (see the comment in FreeNativeOverlappedIfItIsSafe) and an EventHandle to signal the completion.
+        // We need to store the reference count (see the comment in ReleaseRefCount) and an EventHandle to signal the completion.
         // We could keep these two things separate, but since ManualResetEvent is sealed and we want to avoid any extra allocations, this type has been created.
         // It's basically ManualResetEvent with reference count.
         private sealed class CallbackResetEvent : EventWaitHandle
@@ -780,7 +796,7 @@ namespace System.IO
                 _threadPoolBoundHandle = threadPoolBoundHandle;
             }
 
-            internal unsafe void FreeNativeOverlapped(NativeOverlapped* pOverlapped)
+            internal unsafe void ReleaseRefCount(NativeOverlapped* pOverlapped)
             {
                 // Each SafeFileHandle opened for async IO is bound to ThreadPool.
                 // It requires us to provide a callback even if we want to use EventHandle and use GetOverlappedResult to obtain the result.