From 7c8b5dd650b798f91854af61aa51fa03b3ddb63e Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 21 Sep 2026 13:52:12 +0200 Subject: [PATCH] Disable file locking by default on iOS and tvOS (#134060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Disables advisory file locking by default on iOS and tvOS (device and simulator), while leaving Mac Catalyst — and every other platform — unchanged. The behavior remains fully configurable: setting `System.IO.DisableFileLocking=false` (or `DOTNET_SYSTEM_IO_DISABLEFILELOCKING=0`) restores locking on those platforms. ## Motivation On iOS and tvOS, RunningBoard terminates a suspended app with exception code `0xdead10cc` when the app still holds a file lock on a file outside its data container. .NET acquires such a lock implicitly: every `FileStream` opened on Unix takes an advisory `flock` to emulate Windows `FileShare` semantics, and a `FileShare.Read` open maps to a shared `LOCK_SH`. A common way to hit this is stack trace symbolication: when the runtime opens a PDB from inside the read-only app bundle to produce line numbers for an exception, the resulting shared lock lives for as long as the handle does. If the user backgrounds the app while that handle is open, the system kills the process. The lock provides no real value here — the app bundle is read-only and the lock is only advisory between .NET processes — but the termination is fatal and hard to diagnose. ## Implementation `SafeFileHandle.DisableFileLocking` on Unix already consults the `System.IO.DisableFileLocking` / `DOTNET_SYSTEM_IO_DISABLEFILELOCKING` config switch; this change only alters the `defaultValue` passed to that lookup, so it becomes `true` on iOS and tvOS: ```csharp defaultValue: (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS() ``` Notes on the predicate: - `OperatingSystem.IsIOS()` returns `true` for Mac Catalyst as well, so Mac Catalyst is explicitly excluded — it runs on macOS, is not subject to RunningBoard suspension policy, and keeps its current locking behavior. - iOS and tvOS simulators report as iOS/tvOS, so they follow the device default. That keeps simulator test runs representative of device behavior rather than silently diverging. - Browser and WASI continue to force locking off unconditionally, as before; they are unaffected by this change. Because this goes through `AppContextConfigHelper`, an explicit `false` still wins over the new default, so anyone depending on `FileShare` enforcement on iOS/tvOS can opt back in. The observable consequence is that on iOS and tvOS, `FileShare` is no longer enforced between processes or handles: opening a file with `FileShare.None` no longer prevents a second open, and the `IOException` that previously signalled a sharing violation is no longer thrown. This matches the behavior that Browser and WASI have had for some time. ## Test changes Add a small iOS/tvOS-specific test asserting that file locking is in fact off by default, so a future regression in the predicate is caught directly rather than only as a cascade of sharing-violation test failures. Most lock-dependent tests in `System.IO.FileSystem` were already conditioned on `PlatformDetection.IsFileLockingEnabled`, which reflects the runtime's `DisableFileLocking` value and therefore picks up the new default automatically. The second commit extends that same pattern to the remaining tests that depend on `FileShare` being enforced: | Area | Change | |-------------------------------------|---------------------------------------------------------------------------------------------| | `System.Reflection` | `GetAssemblyName_LockedFile` now uses `ConditionalFact` on `IsFileLockingEnabled` | | `System.IO.IsolatedStorage` | `OpenFile_PassesFileShare` now uses `ConditionalFact` on `IsFileLockingEnabled` | | `System.IO.MemoryMappedFiles` | `FileInUse_CreateFromFile_FailsWithExistingNoShareFile` now uses `ConditionalFact` | | `System.ComponentModel.Composition` | The four `ConstructorN_LockedFileAsCodeBaseArgument_ShouldThrowFileLoad` tests use `ConditionalFact` | In two of these cases the new condition subsumes an existing `[SkipOnPlatform(TestPlatforms.Browser, ...)]` attribute — Browser was skipped precisely because it does not honor file locking — so the attribute is removed in favor of the more general check. ## Validation Built and ran locally on macOS: - `./build.sh clr+libs -rc checked` - `./build.sh clr.corelib+clr.nativecorelib+libs.pretest -rc checked` Test results: | Suite | Total | Failed | |------------------------------------------------|-------|--------| | `System.IO.FileSystem.Tests` | 9,607 | 0 | | `System.IO.FileSystem.Tests` (locking disabled) | 2,095 | 0 | | `System.Reflection` tests | 1,778 | 0 | | `System.IO.IsolatedStorage` tests | 198 | 0 | | `System.IO.MemoryMappedFiles` tests | 937 | 0 | | `System.ComponentModel.Composition` tests | 1,431 | 0 | The "locking disabled" run exercises the new code path by forcing `DisableFileLocking` on, which is the configuration iOS and tvOS will now get by default. Fixes #133697. > [!NOTE] > This pull request description was generated with the assistance of GitHub Copilot. --------- Co-authored-by: Vitek Karas <10670590+vitek-karas@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Hosting/AssemblyCatalogTests.cs | 8 +++---- .../IO/IsolatedStorage/OpenFileTests.cs | 2 +- .../MemoryMappedFile.CreateFromFile.Tests.cs | 3 +-- .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 7 ++++-- .../DisabledFileLockingSwitchTests.cs | 5 +++- ...ileSystem.DisabledFileLocking.Tests.csproj | 6 ++++- .../FileLockingDefaults.cs | 24 +++++++++++++++++++ .../System.IO.FileSystem.Tests.csproj | 1 + .../AssemblyNameTests.cs | 3 +-- 9 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileLockingDefaults.cs diff --git a/src/libraries/System.ComponentModel.Composition/tests/System/ComponentModel/Composition/Hosting/AssemblyCatalogTests.cs b/src/libraries/System.ComponentModel.Composition/tests/System/ComponentModel/Composition/Hosting/AssemblyCatalogTests.cs index fd3d3c13f00557..a593b48ef9792d 100644 --- a/src/libraries/System.ComponentModel.Composition/tests/System/ComponentModel/Composition/Hosting/AssemblyCatalogTests.cs +++ b/src/libraries/System.ComponentModel.Composition/tests/System/ComponentModel/Composition/Hosting/AssemblyCatalogTests.cs @@ -174,7 +174,7 @@ public void Constructor1_ValueAsCodebaseArgument_ShouldSetAssemblyProperty() }); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void Constructor1_LockedFileAsCodeBaseArgument_ShouldThrowFileLoad() { AssemblyCatalogConstructorTests.Constructor_LockedFileAsCodeBaseArgument_ShouldThrowIOException((s) => @@ -273,7 +273,7 @@ public void Constructor2_ValueAsCodebaseArgument_ShouldSetAssemblyProperty() }); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void Constructor2_LockedFileAsCodeBaseArgument_ShouldThrowFileLoad() { AssemblyCatalogConstructorTests.Constructor_LockedFileAsCodeBaseArgument_ShouldThrowIOException((s) => @@ -371,7 +371,7 @@ public void Constructor3_ValueAsCodebaseArgument_ShouldSetAssemblyProperty() }); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void Constructor3_LockedFileAsCodeBaseArgument_ShouldThrowFileLoad() { AssemblyCatalogConstructorTests.Constructor_LockedFileAsCodeBaseArgument_ShouldThrowIOException((s) => @@ -468,7 +468,7 @@ public void Constructor4_ValueAsCodebaseArgument_ShouldSetAssemblyProperty() }); } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void Constructor4_LockedFileAsCodeBaseArgument_ShouldThrowFileLoad() { AssemblyCatalogConstructorTests.Constructor_LockedFileAsCodeBaseArgument_ShouldThrowIOException((s) => diff --git a/src/libraries/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/OpenFileTests.cs b/src/libraries/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/OpenFileTests.cs index 8ef2bbbe9be8b9..a50d88c2947d20 100644 --- a/src/libraries/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/OpenFileTests.cs +++ b/src/libraries/System.IO.IsolatedStorage/tests/System/IO/IsolatedStorage/OpenFileTests.cs @@ -66,7 +66,7 @@ public void OpenFile_RaisesIsolatedStorageException() } } - [Fact] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void OpenFile_PassesFileShare() { TestHelper.WipeStores(); diff --git a/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs b/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs index fead66f0362ed9..6d90a155f0cb8a 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs +++ b/src/libraries/System.IO.MemoryMappedFiles/tests/MemoryMappedFile.CreateFromFile.Tests.cs @@ -743,8 +743,7 @@ public void FileInUse_CreateFromFile_FailsWithExistingReadWriteMap() /// /// Test exceptional behavior when trying to create a map for a non-shared file that's currently in use. /// - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "the emscripten implementation ignores FileShare.None")] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public void FileInUse_CreateFromFile_FailsWithExistingNoShareFile() { // Already opened with a FileStream diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 367e90da48fd56..d19918f91747c9 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -34,8 +34,11 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid UnixFileMode.OtherRead | UnixFileMode.OtherWrite; - internal static bool DisableFileLocking { get; } = OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()// #40065: Emscripten does not support file locking - || AppContextConfigHelper.GetBooleanConfig("System.IO.DisableFileLocking", "DOTNET_SYSTEM_IO_DISABLEFILELOCKING", defaultValue: false); + internal static bool DisableFileLocking { get; } = OperatingSystem.IsBrowser() || OperatingSystem.IsWasi() // #40065: Emscripten does not support file locking + || AppContextConfigHelper.GetBooleanConfig( + "System.IO.DisableFileLocking", + "DOTNET_SYSTEM_IO_DISABLEFILELOCKING", + defaultValue: (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || OperatingSystem.IsTvOS()); // not using bool? as it's not thread safe private NullableBool _supportsRandomAccess /* = NullableBool.Undefined */; diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/DisabledFileLockingSwitchTests.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/DisabledFileLockingSwitchTests.cs index 06495b535242fb..768b7334e537df 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/DisabledFileLockingSwitchTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/DisabledFileLockingSwitchTests.cs @@ -11,7 +11,10 @@ public class DisabledFileLockingSwitchTests [Fact] public static void ConfigSwitchIsHonored() { - Assert.Equal(OperatingSystem.IsWindows(), PlatformDetection.IsFileLockingEnabled); + bool expected = OperatingSystem.IsWindows() || + (OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst()) || + OperatingSystem.IsTvOS(); + Assert.Equal(expected, PlatformDetection.IsFileLockingEnabled); } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/System.IO.FileSystem.DisabledFileLocking.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/System.IO.FileSystem.DisabledFileLocking.Tests.csproj index 2cb5e82002d3fb..ce60242a61ed29 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/System.IO.FileSystem.DisabledFileLocking.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/DisabledFileLockingTests/System.IO.FileSystem.DisabledFileLocking.Tests.csproj @@ -38,6 +38,10 @@ - + + + diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileLockingDefaults.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileLockingDefaults.cs new file mode 100644 index 00000000000000..e27782b7610929 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileLockingDefaults.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.IO.Tests +{ + public class FileLockingDefaults + { + [Fact] + [PlatformSpecific(TestPlatforms.iOS | TestPlatforms.tvOS)] + public void FileLockingDisabledByDefault() + { + Assert.False(PlatformDetection.IsFileLockingEnabled); + } + + [Fact] + [PlatformSpecific(TestPlatforms.MacCatalyst)] + public void FileLockingEnabledByDefault() + { + Assert.True(PlatformDetection.IsFileLockingEnabled); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index e5a8faa34e7681..2aa5785709718c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -210,6 +210,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs b/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs index a9d4c2f12b7013..61067b7bd26e16 100644 --- a/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs +++ b/src/libraries/System.Runtime/tests/System.Reflection.Tests/AssemblyNameTests.cs @@ -303,8 +303,7 @@ public static void GetAssemblyName() } } - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "File locking is not respected")] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsFileLockingEnabled))] public static void GetAssemblyName_LockedFile() { using (var tempFile = new TempFile(Path.GetTempFileName(), 100))