Skip to content

On macOS, an application that calls CreateDefaultBuilder() starts a FileSystemWatcher on the system root directory #121256

@awolbach

Description

@awolbach

Description

A little bit of background first. A .NET application that calls Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() to build a host and does not configure the ContentRootPath property will use the application's current directory as its content root. If the application also does not configure the hostBuilder:reloadConfigOnChange setting to be false, the HostBuilder will enable a FileSystemWatcher on that content root directory that watches for changes to appsettings.json and appsettings.[EnvironmentName].json files, with the IncludeSubdirectories option set to true.

On macOS, when any application is launched by launchd such as from the Finder application or the Dock, launchd sets the application's current directory to the root directory (/). This causes a .NET application that uses HostBuilder in the described manner to start up a FileSystemWatcher that then watches the entire file system for changes.

Setting aside the absurdity of the default host looking for its configuration starting from the root directory, I see two problems with this behavior:

  1. It causes significant overhead to attempt to detect and respond to every change within the file system.
  2. Because some of the changes reported by the watcher will be within directories owned by other applications, and the outer PhysicalFilesWatcher handler performs an existence check on each changed file (a SystemNative_LStat call), this is detected by the system as the application attempting to read another application's data which it warns the user about with a dialog. These dialogs state "[Application] would like to access data from other apps." (if the file is within another app's ~/Library/Containers/ sandbox) or "[Application] wants to access files managed by [OneDrive/etc]." (if the file is within part of the file system provided by a File Provider). This suggests to the user that the application is behaving inappropriately, sometimes alarmingly so.

Obviously this can be easily worked around once you're aware of the behavior, so half of the reason for this post is just to raise awareness. But as a pre-configured default, watching from the file system root seems to be just about the worst possible behavior to give the host. It is hard to figure out what is going on when the system begins reporting that your application is accessing other applications' files.

Reproduction Steps

The hard part of reproducing this is just creating an app bundle. To make that easier, I've attached a small HelloWorld console app that uses the Dotnet.Bundle package to make the app bundling easy from the command line. Just unzip, cd into the .csproj folder, and execute these two commands (replacing osx-x64 with your architecture):

dotnet restore -r osx-x64
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:UseAppHost=true

The app just creates a default HostBuilder and then sleeps for 30 seconds, which should be long enough to attach lldb. After building, go into the app bundle and add the get-task-allow entitlement to Info.plist allow a debugger to be attached:

<key>com.apple.security.get-task-allow</key>
<true/>

And then navigate to the application in Finder, launch it, and attach lldb. Here's a brief set of commands to show that the watcher is watching /:

(lldb) sos dumpheap -type System.IO.FileSystemWatcher
         Address               MT           Size
...
Statistics:
          MT Count TotalSize Class Name
...
00011186ee28     1       120 System.IO.FileSystemWatcher
...

(lldb) sos dumpheap -mt 00011186ee28
         Address               MT           Size
    000187467338     00011186ee28            120 
...

(lldb) sos dumpobj 000187467338
Name:        System.IO.FileSystemWatcher
...
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
...
000000011124d7a8  40000aa       20        System.String  0 instance 00000001874649c8 _directory
...

(lldb) sos dumpobj 00000001874649c8
Name:        System.String
...
String:      /
...

(lldb)

HelloWorld.zip

Expected behavior

On macOS, a HostBuilder that uses the pre-configured defaults creates a host that does not query files in another application's sandbox.

Actual behavior

On macOS, a HostBuilder that uses the pre-configured defaults creates a host that routinely queries files in another application's sandbox.

Regression?

Unsure.

Known Workarounds

The obvious workaround is to call HostingHostBuilderExtensions.UseContentRoot(IHostBuilder, String) and explicitly pass it the configuration directory, or failing that, something like AppContext.BaseDirectory, so that the HostBuilder watches for configuration file changes within a much more specific directory. A call to Directory.SetCurrentDirectory(String) before building the host would avoid the problem in a similar manner.

If the application is not configured via appsettings.json, passing the command line argument hostBuilder:reloadConfigOnChange=false (link) to Host.CreateDefaultBuilder() prevents the FileSystemWatcher from being started at all.

Or you could just not use the pre-configured defaults for HostBuilder.

Configuration

I've reproduced the issue on these combinations:

.NET version: 8.0 (8.0.415)
macOS version: 26.0.1 (Tahoe), 15.7.1 (Sequoia), 14.8.1 (Sonoma)
Reproduced on on x64 and ARM64

The issue does not appear to be configuration-specific.

Other information

Here's the call stack that I believe is triggering the system dialogs. Instruments reports this as an lstat64 operation; one example file is: /Users/awolbach/Library/Containers/com.microsoft.teams2/Data/Library/Application Support/Microsoft/MSTeams/EBWebView/WV2Profile_tfw/WebStorage/4/IndexedDB.leveldb/LOG:

lstat$INODE64
SystemNative_LStat
Interop+Sys.LStat(Byte ByRef, FileStatus ByRef)
Interop+Sys.LStat(System.ReadOnlySpan`1<Char>, FileStatus ByRef
System.IO.FileStatus.RefreshCaches(Microsoft.Win32.SafeHandles.SafeFileHandle, System.ReadOnlySpan`1<Char>)
System.IO.FileInfo.get_Exists()
Microsoft.Extensions.FileProviders.Physical.FileSystemInfoHelper.IsExcluded(System.IO.FileSystemInfo, Microsoft.Extensions.FileProviders.Physical.ExclusionFilters)
Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.OnFileSystemEntryChange(System.String)
System.IO.FileSystemWatcher.NotifyFileSystemEventArgs(System.IO.WatcherChangeTypes, System.ReadOnlySpan`1<Char>)
System.IO.FileSystemWatcher+RunningInstance.ProcessEvents(Int32, Byte**, System.Span`1<FSEventStreamEventFlags>, System.Span`1<UInt64>, System.IO.FileSystemWatcher)
System.IO.FileSystemWatcher+RunningInstance+<>c__DisplayClass14_0.<FileSystemEventCallback>b__0(System.Object)
System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
System.IO.FileSystemWatcher+RunningInstance.FileSystemEventCallback(IntPtr, IntPtr, IntPtr, Byte**, FSEventStreamEventFlags*, UInt64*)
implementation_callback_rpc
_Xcallback_rpc  
FSEventsD2F_server  
FSEventsClientProcessMessageCallback  
__CFMachPortPerform  
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
__CFRunLoopDoSource1  
__CFRunLoopRun  
_CFRunLoopRunSpecificWithOptions  
CFRunLoopRun  
System.IO.FileSystemWatcher+RunningInstance+StaticWatcherRunLoopManager.WatchForFileSystemEventsThreadStart(System.Threading.ManualResetEventSlim, Microsoft.Win32.SafeHandles.SafeEventStreamHandle)
System.Threading.Thread.StartCallback()
CallDescrWorkerInternal  
DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int)
ThreadNative::KickOffThread_Worker(void*)  
ManagedThreadBase_DispatchOuter(ManagedThreadCallState*)  
ManagedThreadBase::KickOff(void (*)(void*), void*)  
ThreadNative::KickOffThread(void*)  
CorUnix::CPalThread::ThreadEntry(void*)  
_pthread_start  
thread_start

In case it helps accelerate the investigation, here's the call stack that instantiates the FileSystemWatcher as the JsonConfigurationSource for appsettings.json is built:

System.IO.FileSystem.Watcher.dll!System.IO.FileSystemWatcher.FileSystemWatcher(string path)
Microsoft.Extensions.FileProviders.Physical.dll!Microsoft.Extensions.FileProviders.PhysicalFileProvider.CreateFileWatcher()
System.Private.CoreLib.dll!System.Threading.LazyInitializer.EnsureInitializedCore<System.__Canon>(ref System.__Canon target, ref bool initialized, ref object syncLock, System.Func<System.__Canon> valueFactory)	
System.Private.CoreLib.dll!System.Threading.LazyInitializer.EnsureInitialized<Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher>(ref Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher target, ref bool initialized, ref object syncLock, System.Func<Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher> valueFactory)
Microsoft.Extensions.FileProviders.Physical.dll!Microsoft.Extensions.FileProviders.PhysicalFileProvider.Watch(string filter)
Microsoft.Extensions.Configuration.FileExtensions.dll!Microsoft.Extensions.Configuration.FileConfigurationProvider..ctor.AnonymousMethod__1_0()
Microsoft.Extensions.Primitives.dll!Microsoft.Extensions.Primitives.ChangeToken.ChangeTokenRegistration<System.Action>.ChangeTokenRegistration(System.Func<Microsoft.Extensions.Primitives.IChangeToken> changeTokenProducer, System.Action<System.Action> changeTokenConsumer, System.Action state)
Microsoft.Extensions.Primitives.dll!Microsoft.Extensions.Primitives.ChangeToken.OnChange(System.Func<Microsoft.Extensions.Primitives.IChangeToken> changeTokenProducer, System.Action changeTokenConsumer)	
Microsoft.Extensions.Configuration.FileExtensions.dll!Microsoft.Extensions.Configuration.FileConfigurationProvider.FileConfigurationProvider(Microsoft.Extensions.Configuration.FileConfigurationSource source)
Microsoft.Extensions.Configuration.Json.dll!Microsoft.Extensions.Configuration.Json.JsonConfigurationSource.Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder)
Microsoft.Extensions.Configuration.dll!Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
Microsoft.Extensions.Hosting.dll!Microsoft.Extensions.Hosting.HostBuilder.InitializeAppConfiguration()
Microsoft.Extensions.Hosting.dll!Microsoft.Extensions.Hosting.HostBuilder.Build()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions