From c28805aec3e73a315a15cbe1bf870f6dd422452d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 18 Mar 2026 13:55:40 -0500 Subject: [PATCH] Fix FileSystemWatcher startup race on Linux Ensure the inotify processing thread is ready to read events before Start() returns. Without this synchronization, filesystem events that occur immediately after Start() can be missed if they happen before ProcessEvents() enters its read loop. The race was introduced by #117148 (single shared inotify instance refactor) and manifests as missed Created events in SymbolicLink tests, particularly under jitstress on slower hardware (arm32). Add a ManualResetEventSlim that ProcessEvents() signals before entering the TryReadEvent loop, and that Start() waits on after releasing the watchers lock. For shared inotify instances (subsequent watchers reusing an existing instance), the event is already set so the wait is a no-op. Fixes #125737 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/IO/FileSystemWatcher.Linux.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs index 07edfcf2e2a2d0..bb08525cbd1e10 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs @@ -133,6 +133,7 @@ private sealed class INotify private readonly SafeFileHandle _inotifyHandle; private readonly ConcurrentDictionary _wdToWatch = new ConcurrentDictionary(); private readonly ReaderWriterLockSlim _addLock = new(LockRecursionPolicy.NoRecursion); + private readonly ManualResetEventSlim _readyToRead = new(false); private bool _isThreadStopping; private bool _allWatchersStopped; private int _bufferAvailable; @@ -200,6 +201,11 @@ private void StartThread() } } + /// + /// Waits until the processing thread is ready to read inotify events. + /// + internal void WaitForReadReady() => _readyToRead.Wait(); + private void StopINotify() { // This method gets called only on the ProcessEvents thread, or when that thread fails to start. @@ -489,6 +495,11 @@ private void ProcessEvents() uint movedFromCookie = 0; bool movedFromIsDir = false; + // Signal that the processing thread is ready to read events. + // This ensures callers of Start() can wait until events will + // be captured before performing filesystem operations. + _readyToRead.Set(); + while (TryReadEvent(out NotifyEvent nextEvent)) { if (!ProcessEvent(nextEvent, ref movedFromWatchCount, ref movedFromName, ref movedFromCookie, ref movedFromIsDir)) @@ -509,6 +520,9 @@ private void ProcessEvents() } finally { + // Ensure any caller blocked on WaitForReadReady() is unblocked, + // even if ProcessEvents exits early due to an error. + _readyToRead.Set(); Debug.Assert(_inotifyHandle.IsClosed); } } @@ -1055,6 +1069,12 @@ public void Start() } } + // Ensure the processing thread is ready to read events before + // we start emitting or performing filesystem operations that + // should be observed. Without this, events can be missed if + // they occur before ProcessEvents enters its read loop. + inotify.WaitForReadReady(); + _ = DequeueEvents(); if (rootDirectory is not null && IncludeSubdirectories)