-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Closed
Labels
Description
Description
[Issue]
FileSystemWatcher leaks the following objects if it is not disposed explicitly:
- System.Threading.ThreadPoolBoundHandleOverlapped
- System.Threading.ThreadPoolBoundHandle
- System.Threading.PreAllocatedOverlapped
- Microsoft.Win32.SafeHandles.SafeFileHandle
- System.IO.FileSystemWatcher+AsyncReadState
- System.Byte[]
This issue still occurs with .NET 10 Preview 5.
[Cause]
ReadDirectoryChangesCallback is not called to free System.Threading.ThreadPoolBoundHandleOverlapped._nativeOverlapped because FileSystemWatcher is already garbage-collected.
[Debugging]
When it is disposed explicitly, System.Threading.ThreadPoolBoundHandleOverlapped._nativeOverlapped is freed as follows:
0:010> !clrstack
OS Thread Id: 0x77d4 (10)
Child SP IP Call Site
0000007A3E47F6E8 00007ff8ef6adce9 System.Threading.PreAllocatedOverlapped.IDeferredDisposableOnFinalReleasePortableCore(Boolean)
0000007A3E47F6F0 00007ff8ef6adbaa System.Threading.PreAllocatedOverlapped.Dispose()
0000007A3E47F720 00007ff977ac68f4 System.IO.FileSystemWatcher.Monitor(AsyncReadState)
0000007A3E47F7A0 00007ff977ac6bad System.IO.FileSystemWatcher.ReadDirectoryChangesCallback(UInt32, UInt32, AsyncReadState)
0000007A3E47F810 00007ff977ac744d System.IO.FileSystemWatcher+c.b__85_0(UInt32, UInt32, System.Threading.NativeOverlapped*)
0000007A3E47F860 00007ff8ef6b281c System.Threading.PortableThreadPool+IOCompletionPoller+Callback.Invoke(Event)
0000007A3E47F930 00007ff8ef8fca43 System.Threading.ThreadPoolTypedWorkItemQueue`2[[System.Threading.PortableThreadPool+IOCompletionPoller+Event, System.Private.CoreLib],[System.Threading.PortableThreadPool+IOCompletionPoller+Callback, System.Private.CoreLib]].System.Threading.IThreadPoolWorkItem.Execute()
0000007A3E47F9D0 00007ff8ef6a69a2 System.Threading.ThreadPoolWorkQueue.Dispatch()
0000007A3E47FA60 00007ff8ef6b3441 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
0000007A3E47FD98 00007ff8f02d7c43 [DebuggerU2MCatchHandlerFrame: 0000007a3e47fd98]
private unsafe void IDeferredDisposableOnFinalReleasePortableCore(bool disposed)
{
if (_overlappedPortableCore != null) // protect against ctor throwing exception and leaving field uninitialized
{
if (disposed)
{
Overlapped.Free(_overlappedPortableCore._nativeOverlapped);
}
…
0:010> !clrstack -p
OS Thread Id: 0x77d4 (10)
Child SP IP Call Site
0000007A3E47F6E8 00007ff8ef6adce9 System.Threading.PreAllocatedOverlapped.IDeferredDisposableOnFinalReleasePortableCore(Boolean)
PARAMETERS:
this = <no data>
disposed (<CLR reg>) = 0x0000000000000001
0000007A3E47F6F0 00007ff8ef6adbaa System.Threading.PreAllocatedOverlapped.Dispose()
PARAMETERS:
this (<CLR reg>) = 0x0000025de10caa20
…
0:010> !DumpObj /d 0000025de10caa20
Name: System.Threading.PreAllocatedOverlapped
MethodTable: 00007ff890929230
Canonical MethodTable: 00007ff890929230
Tracked Type: false
Size: 40(0x28) bytes
File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.6\System.Private.CoreLib.dll
Fields:
MT Field Offset Type VT Attr Value Name
0000000000000000 4000e87 10 PTR 0 instance 0000000000000000 _overlappedWindowsThreadPool
00007ff8909450d0 4000e88 18 ...Private.CoreLib]] 1 instance 0000025de10caa38 _lifetime
00007ff890944010 4000e89 8 ...dHandleOverlapped 0 instance 0000025de10caa48 _overlappedPortableCore
0:010> !DumpObj /d 0000025de10caa48
Name: System.Threading.ThreadPoolBoundHandleOverlapped
…
However, when it is not disposed explicitly and is disposed by GC, ReadDirectoryChangesCallback is not called because FileSystemWatcher is already garbage-collected:
0:010> .frame 4
04 0000007a`3e47f810 00007ff8`ef6b281c System_IO_FileSystem_Watcher!System.IO.FileSystemWatcher.<>c.<StartRaisingEvents>b__85_0+0x6d [/_/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Win32.cs @ 68]
AsyncReadState state = (AsyncReadState)ThreadPoolBoundHandle.GetNativeOverlappedState(overlappedPointer)!;
state.ThreadPoolBinding.FreeNativeOverlapped(overlappedPointer);
if (state.WeakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
watcher.ReadDirectoryChangesCallback(errorCode, numBytes, state);
}
Reproduction Steps
- Run FileSystemWatcherIssue.zip\bin\debug\net9.0\FileSystemWatcherIssue.exe.
- Press enter to force GC.
- Attach a debugger to the process and run: !dumpheap -stat -live.
- Press enter to repeat the steps to confirm each loop increases them by 50.
Expected behavior
Don't see 50 instances of each of the followings:
- System.Threading.ThreadPoolBoundHandleOverlapped
- System.Threading.ThreadPoolBoundHandle
- System.Threading.PreAllocatedOverlapped
- Microsoft.Win32.SafeHandles.SafeFileHandle
- System.IO.FileSystemWatcher+AsyncReadState
- System.Byte[]
Actual behavior
See 50 instances of each:
7ff89ef7cbe8 51 2,040 System.Threading.ThreadPoolBoundHandle
7ff89ef75f68 52 2,080 System.Threading.PreAllocatedOverlapped
…
7ff89ef723e0 50 3,200 System.IO.FileSystemWatcher+AsyncReadState
7ff89ed9a3a0 50 3,600 Microsoft.Win32.SafeHandles.SafeFileHandle
…
7ff89ef7edd0 52 5,408 System.Threading.ThreadPoolBoundHandleOverlapped
…
7ff89eddb420 55 415,617 System.Byte[]
Regression?
Not a regression.
Known Workarounds
Dispose it explicitly.
Configuration
No response
Other information
No response