From 7b8803afac057274e6f11f6fc4df0af2173754e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:41:42 +0000 Subject: [PATCH 1/4] Initial plan From 7c4acc0956d5058d18082a0c8a9eb0ca41c262a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:07:10 +0000 Subject: [PATCH 2/4] Fix EEXIST race in CreateOrOpenFile; add ConcurrentCreation regression test Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d046e769-8640-4561-9aaf-84ae5cf1d9b0 Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 39 +++++++++++++++++- .../System.Threading/tests/MutexTests.cs | 41 +++++++++++++++++++ 2 files changed, 79 insertions(+), 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 bd8a97d35ee0c9..d5273119eee05b 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 @@ -474,7 +474,44 @@ 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 = 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) + { + 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)); + } + } + + createdFile = false; + return fd; } int result = Interop.Sys.FChMod(fd, (int)permissionsMask); diff --git a/src/libraries/System.Threading/tests/MutexTests.cs b/src/libraries/System.Threading/tests/MutexTests.cs index d145b97e314e52..f11dd0a3b9a6c4 100644 --- a/src/libraries/System.Threading/tests/MutexTests.cs +++ b/src/libraries/System.Threading/tests/MutexTests.cs @@ -1062,6 +1062,47 @@ 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() + { + // Regression test for https://github.com/dotnet/runtime/issues/TBD + // 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") }; From 5df527e4642035337f79b3b8efe06b41ad3c1d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:08:34 +0000 Subject: [PATCH 3/4] Remove placeholder TBD issue reference from test comment Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d046e769-8640-4561-9aaf-84ae5cf1d9b0 Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> --- src/libraries/System.Threading/tests/MutexTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Threading/tests/MutexTests.cs b/src/libraries/System.Threading/tests/MutexTests.cs index f11dd0a3b9a6c4..9e0f5d342fc5da 100644 --- a/src/libraries/System.Threading/tests/MutexTests.cs +++ b/src/libraries/System.Threading/tests/MutexTests.cs @@ -1067,7 +1067,6 @@ public void NamedMutex_TooSmallSharedMemoryFile() [ActiveIssue("https://github.com/dotnet/runtime/issues/96191", TestPlatforms.Browser)] public void NamedMutex_ConcurrentCreation_NoException() { - // Regression test for https://github.com/dotnet/runtime/issues/TBD // 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. From 2edb0c077ed3dab0f0d502bbea177dcb15d0146f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:29:17 +0000 Subject: [PATCH 4/4] Refactor: extract ValidateUserScopeOwnership local function; add missing fd.Dispose() before reassignment Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d046e769-8640-4561-9aaf-84ae5cf1d9b0 Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> --- .../src/System/IO/SharedMemoryManager.Unix.cs | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 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 d5273119eee05b..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; @@ -481,6 +464,7 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha // 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) { @@ -490,24 +474,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; @@ -526,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)