From ecfbb15207fe4dfb327450db38076f48a868ce4f Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Mon, 13 Jul 2026 19:22:30 +0200 Subject: [PATCH 1/3] Raise InternalBufferOverflowException for FSEvents overflow on macOS On macOS, an FSEvents buffer/rescan overflow raised a plain IOException with the unformatted SR.FSW_BufferOverflow string (so the message literally contained "{0}"). Windows and Linux both use the shared CreateBufferOverflowException helper, which produces an InternalBufferOverflowException with a properly formatted message. Use that helper on macOS too so the overflow exception type and message are consistent across platforms, while preserving the FSEvents flags in HResult for diagnostics as the previous IOException did. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aeef6f66-c627-4a58-a4cb-6d14cf4d2649 --- .../src/System/IO/FileSystemWatcher.OSX.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs index 86294f879b39d3..3af573ca11a3b3 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs @@ -456,7 +456,9 @@ private unsafe void ProcessEvents(int numEvents, // First, we should check if this event should kick off a re-scan since we can't really rely on anything after this point if that is true if (ShouldRescanOccur(eventFlags[i])) { - watcher.OnError(new ErrorEventArgs(new IOException(SR.FSW_BufferOverflow, (int)eventFlags[i]))); + InternalBufferOverflowException exception = CreateBufferOverflowException(_fullDirectory); + exception.HResult = (int)eventFlags[i]; + watcher.OnError(new ErrorEventArgs(exception)); break; } else if (handledRenameEvents == i) From f113fa84ee50700d06165ffcd8dd67509f29fcfc Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Thu, 16 Jul 2026 17:49:04 +0200 Subject: [PATCH 2/3] Don't preserve eventFlags in exception HResult --- .../src/System/IO/FileSystemWatcher.OSX.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs index 3af573ca11a3b3..16906d0e9fee40 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs @@ -456,9 +456,7 @@ private unsafe void ProcessEvents(int numEvents, // First, we should check if this event should kick off a re-scan since we can't really rely on anything after this point if that is true if (ShouldRescanOccur(eventFlags[i])) { - InternalBufferOverflowException exception = CreateBufferOverflowException(_fullDirectory); - exception.HResult = (int)eventFlags[i]; - watcher.OnError(new ErrorEventArgs(exception)); + watcher.OnError(new ErrorEventArgs(CreateBufferOverflowException(_fullDirectory))); break; } else if (handledRenameEvents == i) From 32eec10b5bed92aaa97258ba3de1544c398f0a84 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Thu, 16 Jul 2026 18:28:38 +0200 Subject: [PATCH 3/3] Add macOS test for InternalBufferOverflowException on rescan Add an OSX-only OuterLoop test that mounts a disk image inside the watched directory. FSEvents reports a Mount flag for that path, which ShouldRescanOccur treats like a buffer overflow and surfaces through the Error event. This is a deterministic way to reach the rescan-error path (unlike an actual FSEvents buffer overflow, which can't be triggered reliably), and it asserts the exception is InternalBufferOverflowException with a formatted message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aeef6f66-c627-4a58-a4cb-6d14cf4d2649 --- .../FileSystemWatcher.InternalBufferSize.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs index 6df52a9395793e..a1a3516a44a7af 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Sdk; namespace System.IO.Tests { @@ -94,6 +96,59 @@ public void FileSystemWatcher_InternalBufferSize_SynchronizingObject() } } + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + [OuterLoop("Creates, mounts, and unmounts a disk image.")] + public async Task FileSystemWatcher_OSX_MountInWatchedTree_RaisesInternalBufferOverflowException() + { + // Mounting a volume inside the watched tree makes FSEvents report a Mount flag, which the OSX + // FileSystemWatcher surfaces through the Error event as an InternalBufferOverflowException to + // signal that a rescan is required. Unlike an actual buffer overflow, a mount is a + // deterministic way to reach that code path. + string watchedDir = CreateTestDirectory(); + string mountPoint = Path.Combine(watchedDir, "mnt"); + string image = GetTestFilePath() + ".dmg"; + + // Create the mount point up front so the attach below is the only event of interest, and + // create a small HFS+ disk image to mount. Creating and attaching a user-owned image does not + // require elevation on macOS. + Directory.CreateDirectory(mountPoint); + await RunHdiutil("create", "-size", "10m", "-fs", "HFS+", "-volname", "fswtest", "-ov", "-quiet", image); + + using var watcher = new FileSystemWatcher(watchedDir) { IncludeSubdirectories = true }; + + var errorReported = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + watcher.Error += (o, e) => errorReported.TrySetResult(e.GetException()); + + watcher.EnableRaisingEvents = true; + try + { + await RunHdiutil("attach", image, "-mountpoint", mountPoint, "-nobrowse", "-noverify", "-quiet"); + + Exception error = await errorReported.Task.WaitAsync(TimeSpan.FromMilliseconds(LongWaitTimeout)); + Assert.IsType(error); + // The message must be formatted (the directory substituted in), not the raw format string. + Assert.DoesNotContain("{0}", error.Message); + } + finally + { + watcher.EnableRaisingEvents = false; + await RunHdiutil(throwOnError: false, "detach", mountPoint, "-force", "-quiet"); + } + } + + private static Task RunHdiutil(params string[] arguments) => RunHdiutil(throwOnError: true, arguments); + + private static async Task RunHdiutil(bool throwOnError, params string[] arguments) + { + ProcessTextOutput result = await Process.RunAndCaptureTextAsync("hdiutil", arguments); + if (throwOnError && result.ExitStatus.ExitCode != 0) + { + throw new XunitException( + $"hdiutil {string.Join(' ', arguments)} failed with exit code {result.ExitStatus.ExitCode}: {result.StandardError}"); + } + } + #region Test Helpers private FileSystemWatcher CreateWatcher(string testDirectoryPath, string filePath, ManualResetEvent unblockHandler)