From 47fcd6b13c6a219fd544cd9be266011f229ee3d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:22:51 +0000 Subject: [PATCH 1/6] Initial plan From 3955680cd8e4db09fbea6f02fbedbc3847980efe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:50:45 +0000 Subject: [PATCH 2/6] Report progress on mutex race fix Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 250 ++++++++++-------- 1 file changed, 145 insertions(+), 105 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index 6ce05fdd8c467b..31b94623f37e02 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -385,6 +385,7 @@ internal static class SharedMemoryHelpers private const UnixFileMode PermissionsMask_Sticky = UnixFileMode.StickyBit; private const string SharedMemoryUniqueTempNameTemplate = ".dotnet.XXXXXX"; + private const int TransientRetryCount = 10; // See https://developer.apple.com/documentation/Foundation/FileManager/containerURL(forSecurityApplicationGroupIdentifier:)#App-Groups-in-macOS for details on this path. private const string ApplicationContainerBasePathSuffix = "/Library/Group Containers/"; @@ -419,7 +420,6 @@ private static string InitalizeSharedFilesPath() internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, SharedMemoryId id, bool createIfNotExist, out bool createdFile) { - const int TransientRetryCount = 10; int transientRetryCount = 0; while (true) @@ -527,159 +527,199 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId id, bool isGlobalLockAcquired, bool createIfNotExist = true, bool isSystemDirectory = false) { - UnixFileMode permissionsMask = id.IsUserScope - ? PermissionsMask_OwnerUser_ReadWriteExecute - : PermissionsMask_AllUsers_ReadWriteExecute; + int transientRetryCount = 0; + while (true) + { + UnixFileMode permissionsMask = id.IsUserScope + ? PermissionsMask_OwnerUser_ReadWriteExecute + : PermissionsMask_AllUsers_ReadWriteExecute; - int statResult = Interop.Sys.Stat(directoryPath, out Interop.Sys.FileStatus fileStatus); + int statResult = Interop.Sys.Stat(directoryPath, out Interop.Sys.FileStatus fileStatus); - if (statResult != 0 && Interop.Sys.GetLastError() == Interop.Error.ENOENT) - { - if (!createIfNotExist) + if (statResult != 0 && Interop.Sys.GetLastError() == Interop.Error.ENOENT) { - // The directory does not exist and we are not allowed to create it. - return false; - } + if (!createIfNotExist) + { + // The directory does not exist and we are not allowed to create it. + return false; + } - // The path does not exist, create the directory. The permissions mask passed to mkdir() is filtered by the process' - // permissions umask, so mkdir() may not set all of the requested permissions. We need to use chmod() to set the proper - // permissions. That creates a race when there is no global lock acquired when creating the directory. Another user's - // process may create the directory and this user's process may try to use it before the other process sets the full - // permissions. In that case, create a temporary directory first, set the permissions, and rename it to the actual - // directory name. + // The path does not exist, create the directory. The permissions mask passed to mkdir() is filtered by the process' + // permissions umask, so mkdir() may not set all of the requested permissions. We need to use chmod() to set the proper + // permissions. That creates a race when there is no global lock acquired when creating the directory. Another user's + // process may create the directory and this user's process may try to use it before the other process sets the full + // permissions. In that case, create a temporary directory first, set the permissions, and rename it to the actual + // directory name. - if (isGlobalLockAcquired) - { + if (isGlobalLockAcquired) + { #pragma warning disable CA1416 // Validate platform compatibility. This file is only included on Unix platforms. - Directory.CreateDirectory(directoryPath, permissionsMask); + Directory.CreateDirectory(directoryPath, permissionsMask); #pragma warning restore CA1416 // Validate platform compatibility + try + { + FileSystem.SetUnixFileMode(directoryPath, permissionsMask); + } + catch (Exception) + { + Directory.Delete(directoryPath); + throw; + } + + return true; + } + + string tempPath = Path.Combine(SharedFilesPath, SharedMemoryUniqueTempNameTemplate); + + unsafe + { + byte* tempPathPtr = Utf8StringMarshaller.ConvertToUnmanaged(tempPath); + if (Interop.Sys.MkdTemp(tempPathPtr) == null) + { + Utf8StringMarshaller.Free(tempPathPtr); + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + throw Interop.GetExceptionForIoErrno(error, tempPath); + } + // Convert the path back to get the substituted path. + tempPath = Utf8StringMarshaller.ConvertToManaged(tempPathPtr)!; + Utf8StringMarshaller.Free(tempPathPtr); + } + try { - FileSystem.SetUnixFileMode(directoryPath, permissionsMask); + FileSystem.SetUnixFileMode(tempPath, permissionsMask); } catch (Exception) { - Directory.Delete(directoryPath); + Directory.Delete(tempPath); throw; } - return true; - } + if (Interop.Sys.Rename(tempPath, directoryPath) == 0) + { + return true; + } - string tempPath = Path.Combine(SharedFilesPath, SharedMemoryUniqueTempNameTemplate); + // Another process may have beaten us to it. Delete the temp directory and continue to check the requested directory to + // see if it meets our needs. + Directory.Delete(tempPath); + statResult = Interop.Sys.Stat(directoryPath, out fileStatus); + } - unsafe + // If the path exists, check that it's a directory + if (statResult != 0 || (fileStatus.Mode & Interop.Sys.FileTypes.S_IFDIR) == 0) { - byte* tempPathPtr = Utf8StringMarshaller.ConvertToUnmanaged(tempPath); - if (Interop.Sys.MkdTemp(tempPathPtr) == null) + if (statResult != 0) { - Utf8StringMarshaller.Free(tempPathPtr); Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - throw Interop.GetExceptionForIoErrno(error, tempPath); + if (error.Error == Interop.Error.ENOENT && RetryOnTransientPermissionFailure()) + { + continue; + } + + if (error.Error != Interop.Error.ENOENT) + { + throw Interop.GetExceptionForIoErrno(error, directoryPath); + } + + throw Interop.GetExceptionForIoErrno(error, directoryPath); + } + else + { + throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPath)); } - // Convert the path back to get the substituted path. - tempPath = Utf8StringMarshaller.ConvertToManaged(tempPathPtr)!; - Utf8StringMarshaller.Free(tempPathPtr); } - try + if (isSystemDirectory) { - FileSystem.SetUnixFileMode(tempPath, permissionsMask); + // For system directories (such as TEMP_DIRECTORY_PATH), require sufficient permissions only for the + // owner user. For instance, "docker run --mount ..." to mount /tmp to some directory on the host mounts the + // destination directory with the same permissions as the source directory, which may not include some permissions for + // other users. In the docker container, other user permissions are typically not relevant and relaxing the permissions + // requirement allows for that scenario to work without having to work around it by first giving sufficient permissions + // for all users. + // + // If the directory is being used for user-scoped shared memory data, also ensure that either it has the sticky bit or + // it's owned by the current user and without write access for other users. + + permissionsMask = PermissionsMask_OwnerUser_ReadWriteExecute; + if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask + && ( + !id.IsUserScope || + (fileStatus.Mode & (int)PermissionsMask_Sticky) == (int)PermissionsMask_Sticky || + (fileStatus.Uid == id.Uid && (fileStatus.Mode & (int)PermissionsMask_NonOwnerUsers_Write) == 0) + )) + { + return true; + } + + if (!RetryOnTransientPermissionFailure()) + { + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); + } + + continue; } - catch (Exception) + + // For non-system directories (such as SharedFilesPath/UserUnscopedRuntimeTempDirectoryName), + // require the sufficient permissions and try to update them if requested to create the directory, so that + // shared memory files may be shared according to its scope. + + // For user-scoped directories, verify the owner UID + if (id.IsUserScope && fileStatus.Uid != id.Uid) { - Directory.Delete(tempPath); - throw; + if (!RetryOnTransientPermissionFailure()) + { + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPath, id.Uid)); + } + + continue; } - if (Interop.Sys.Rename(tempPath, directoryPath) == 0) + // Verify the permissions, or try to change them if possible + if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) == (int)permissionsMask + || (createIfNotExist && Interop.Sys.ChMod(directoryPath, (int)permissionsMask) == 0)) { return true; } - // Another process may have beaten us to it. Delete the temp directory and continue to check the requested directory to - // see if it meets our needs. - Directory.Delete(tempPath); - statResult = Interop.Sys.Stat(directoryPath, out fileStatus); - } - - // If the path exists, check that it's a directory - if (statResult != 0 || (fileStatus.Mode & Interop.Sys.FileTypes.S_IFDIR) == 0) - { - if (statResult != 0) + // We were not able to verify or set the necessary permissions. For user-scoped directories, this is treated as a failure + // since other users aren't sufficiently restricted in permissions. + if (id.IsUserScope) { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - if (error.Error != Interop.Error.ENOENT) + if (!RetryOnTransientPermissionFailure()) { - throw Interop.GetExceptionForIoErrno(error, directoryPath); + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPath, Convert.ToString(fileStatus.Mode, 8))); } - } - else - { - throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPath)); - } - } - if (isSystemDirectory) - { - // For system directories (such as TEMP_DIRECTORY_PATH), require sufficient permissions only for the - // owner user. For instance, "docker run --mount ..." to mount /tmp to some directory on the host mounts the - // destination directory with the same permissions as the source directory, which may not include some permissions for - // other users. In the docker container, other user permissions are typically not relevant and relaxing the permissions - // requirement allows for that scenario to work without having to work around it by first giving sufficient permissions - // for all users. - // - // If the directory is being used for user-scoped shared memory data, also ensure that either it has the sticky bit or - // it's owned by the current user and without write access for other users. + continue; + } + // For user-unscoped directories, as a last resort, check that at least the owner user has full access. permissionsMask = PermissionsMask_OwnerUser_ReadWriteExecute; - if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask - && ( - !id.IsUserScope || - (fileStatus.Mode & (int)PermissionsMask_Sticky) == (int)PermissionsMask_Sticky || - (fileStatus.Uid == id.Uid && (fileStatus.Mode & (int)PermissionsMask_NonOwnerUsers_Write) == 0) - )) + if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask) { return true; } - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); + if (!RetryOnTransientPermissionFailure()) + { + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPath, Convert.ToString(fileStatus.Mode, 8))); + } } - // For non-system directories (such as SharedFilesPath/UserUnscopedRuntimeTempDirectoryName), - // require the sufficient permissions and try to update them if requested to create the directory, so that - // shared memory files may be shared according to its scope. - - // For user-scoped directories, verify the owner UID - if (id.IsUserScope && fileStatus.Uid != id.Uid) + bool RetryOnTransientPermissionFailure() { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPath, id.Uid)); - } + if (!createIfNotExist || transientRetryCount >= TransientRetryCount) + { + return false; + } - // Verify the permissions, or try to change them if possible - if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) == (int)permissionsMask - || (createIfNotExist && Interop.Sys.ChMod(directoryPath, (int)permissionsMask) == 0)) - { + transientRetryCount++; + Thread.Sleep(1); return true; } - - // We were not able to verify or set the necessary permissions. For user-scoped directories, this is treated as a failure - // since other users aren't sufficiently restricted in permissions. - if (id.IsUserScope) - { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPath, Convert.ToString(fileStatus.Mode, 8))); - } - - - // For user-unscoped directories, as a last resort, check that at least the owner user has full access. - permissionsMask = PermissionsMask_OwnerUser_ReadWriteExecute; - if ((fileStatus.Mode & (int)permissionsMask) != (int)permissionsMask) - { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPath, Convert.ToString(fileStatus.Mode, 8))); - } - - return true; } internal static MemoryMappedFileHolder MemoryMapFile(SafeFileHandle fileHandle, nuint sharedDataTotalByteCount) From 5b2f3d3ebc67f575b7013da4574bb001a46ae871 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:57:42 +0000 Subject: [PATCH 3/6] Finalize mutex directory race fix Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index 31b94623f37e02..9f32363abf7b34 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -618,11 +618,6 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId continue; } - if (error.Error != Interop.Error.ENOENT) - { - throw Interop.GetExceptionForIoErrno(error, directoryPath); - } - throw Interop.GetExceptionForIoErrno(error, directoryPath); } else From 18f567cca73d04680149fb5a84ebd9d4c88d632a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:57:22 +0000 Subject: [PATCH 4/6] Allow directory transient retry on open path Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index 9f32363abf7b34..96429c33a84321 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -706,7 +706,7 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId bool RetryOnTransientPermissionFailure() { - if (!createIfNotExist || transientRetryCount >= TransientRetryCount) + if (transientRetryCount >= TransientRetryCount) { return false; } From 0486326414283cf0e4ba62285762c62a92524bae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:20:30 +0000 Subject: [PATCH 5/6] Simplify shared memory directory stat flow Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index 96429c33a84321..46a1550f85bdd3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -607,23 +607,21 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId statResult = Interop.Sys.Stat(directoryPath, out fileStatus); } - // If the path exists, check that it's a directory - if (statResult != 0 || (fileStatus.Mode & Interop.Sys.FileTypes.S_IFDIR) == 0) + if (statResult != 0) { - if (statResult != 0) - { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - if (error.Error == Interop.Error.ENOENT && RetryOnTransientPermissionFailure()) - { - continue; - } - - throw Interop.GetExceptionForIoErrno(error, directoryPath); - } - else + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + if (error.Error == Interop.Error.ENOENT && RetryOnTransientPermissionFailure()) { - throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPath)); + continue; } + + throw Interop.GetExceptionForIoErrno(error, directoryPath); + } + + // If the path exists, check that it's a directory + if ((fileStatus.Mode & Interop.Sys.FileTypes.S_IFDIR) == 0) + { + throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPath)); } if (isSystemDirectory) From 2e680d113c4bb6aa30f4f6af4f5321dd7f7144bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:47:57 +0000 Subject: [PATCH 6/6] Refactor transient directory retry flow Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index 46a1550f85bdd3..dab36680708630 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -647,12 +647,12 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId return true; } - if (!RetryOnTransientPermissionFailure()) + if (RetryOnTransientPermissionFailure()) { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); + continue; } - continue; + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); } // For non-system directories (such as SharedFilesPath/UserUnscopedRuntimeTempDirectoryName), @@ -662,12 +662,12 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId // For user-scoped directories, verify the owner UID if (id.IsUserScope && fileStatus.Uid != id.Uid) { - if (!RetryOnTransientPermissionFailure()) + if (RetryOnTransientPermissionFailure()) { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPath, id.Uid)); + continue; } - continue; + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPath, id.Uid)); } // Verify the permissions, or try to change them if possible @@ -681,12 +681,12 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId // since other users aren't sufficiently restricted in permissions. if (id.IsUserScope) { - if (!RetryOnTransientPermissionFailure()) + if (RetryOnTransientPermissionFailure()) { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPath, Convert.ToString(fileStatus.Mode, 8))); + continue; } - continue; + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPath, Convert.ToString(fileStatus.Mode, 8))); } // For user-unscoped directories, as a last resort, check that at least the owner user has full access. @@ -696,10 +696,12 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId return true; } - if (!RetryOnTransientPermissionFailure()) + if (RetryOnTransientPermissionFailure()) { - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPath, Convert.ToString(fileStatus.Mode, 8))); + continue; } + + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPath, Convert.ToString(fileStatus.Mode, 8))); } bool RetryOnTransientPermissionFailure()