Skip to content

FileSystemWatcher leaks objects if it is not disposed explicitly #116768

@ishimada

Description

@ishimada

Description

[Issue]
FileSystemWatcher leaks the following objects if it is not disposed explicitly:

  1. System.Threading.ThreadPoolBoundHandleOverlapped
  2. System.Threading.ThreadPoolBoundHandle
  3. System.Threading.PreAllocatedOverlapped
  4. Microsoft.Win32.SafeHandles.SafeFileHandle
  5. System.IO.FileSystemWatcher+AsyncReadState
  6. 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

  1. Run FileSystemWatcherIssue.zip\bin\debug\net9.0\FileSystemWatcherIssue.exe.
  2. Press enter to force GC.
  3. Attach a debugger to the process and run: !dumpheap -stat -live.
  4. 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:

  1. System.Threading.ThreadPoolBoundHandleOverlapped
  2. System.Threading.ThreadPoolBoundHandle
  3. System.Threading.PreAllocatedOverlapped
  4. Microsoft.Win32.SafeHandles.SafeFileHandle
  5. System.IO.FileSystemWatcher+AsyncReadState
  6. 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions