-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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:
- It causes significant overhead to attempt to detect and respond to every change within the file system.
- 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_LStatcall), 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)
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()