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 bd8a97d35ee0c9..dc6e9d5e2996dc 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 @@ -426,24 +426,7 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha { if (id.IsUserScope) { - if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0) - { - error = Interop.Sys.GetLastErrorInfo(); - fd.Dispose(); - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); - } - - if (fileStatus.Uid != id.Uid) - { - fd.Dispose(); - throw new IOException(SR.Format(SR.IO_SharedMemory_FileNotOwnedByUid, sharedMemoryFilePath, id.Uid)); - } - - if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite) - { - fd.Dispose(); - throw new IOException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite)); - } + ValidateUserScopeOwnership(fd); } createdFile = false; return fd; @@ -474,7 +457,28 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha if (fd.IsInvalid) { error = Interop.Sys.GetLastErrorInfo(); - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + if (error.Error != Interop.Error.EEXIST) + { + throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + } + + // Another process created the file between our initial open (ENOENT) and our + // exclusive-create attempt (EEXIST). Fall back to opening the now-existing file. + fd.Dispose(); + fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0); + if (fd.IsInvalid) + { + error = Interop.Sys.GetLastErrorInfo(); + throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + } + + if (id.IsUserScope) + { + ValidateUserScopeOwnership(fd); + } + + createdFile = false; + return fd; } int result = Interop.Sys.FChMod(fd, (int)permissionsMask); @@ -489,6 +493,28 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha createdFile = true; return fd; + + void ValidateUserScopeOwnership(SafeFileHandle handle) + { + if (Interop.Sys.FStat(handle, out Interop.Sys.FileStatus fileStatus) != 0) + { + Interop.ErrorInfo err = Interop.Sys.GetLastErrorInfo(); + handle.Dispose(); + throw Interop.GetExceptionForIoErrno(err, sharedMemoryFilePath); + } + + if (fileStatus.Uid != id.Uid) + { + handle.Dispose(); + throw new IOException(SR.Format(SR.IO_SharedMemory_FileNotOwnedByUid, sharedMemoryFilePath, id.Uid)); + } + + if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite) + { + handle.Dispose(); + throw new IOException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite)); + } + } } internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId id, bool isGlobalLockAcquired, bool createIfNotExist = true, bool isSystemDirectory = false) diff --git a/src/libraries/System.Threading/tests/MutexTests.cs b/src/libraries/System.Threading/tests/MutexTests.cs index d145b97e314e52..9e0f5d342fc5da 100644 --- a/src/libraries/System.Threading/tests/MutexTests.cs +++ b/src/libraries/System.Threading/tests/MutexTests.cs @@ -1062,6 +1062,46 @@ public void NamedMutex_TooSmallSharedMemoryFile() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/96191", TestPlatforms.Browser)] + public void NamedMutex_ConcurrentCreation_NoException() + { + // Simulates the ENOENT -> EEXIST race in CreateOrOpenFile: multiple threads concurrently + // create the same named mutex. Without the fix, a thread that sees ENOENT on the initial + // open but EEXIST on the exclusive-create attempt would throw IOException. + string name = $"Global\\{Guid.NewGuid():N}"; + var options = new NamedWaitHandleOptions { CurrentSessionOnly = false, CurrentUserOnly = false }; + + const int threadCount = 8; + var exceptions = new Exception[threadCount]; + var barrier = new Barrier(threadCount); + var threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + int idx = i; + threads[idx] = new Thread(() => + { + try + { + barrier.SignalAndWait(); + using var m = new Mutex(false, name, options, out _); + } + catch (Exception ex) + { + exceptions[idx] = ex; + } + }); + threads[idx].IsBackground = true; + } + + foreach (var t in threads) t.Start(); + foreach (var t in threads) t.Join(); + + Assert.All(exceptions, e => Assert.Null(e)); + } + public static TheoryData GetValidNames() { var names = new TheoryData() { Guid.NewGuid().ToString("N") };