Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux - #125524

Closed
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race
Closed

Fix TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Linux#125524
steveisok wants to merge 5 commits into
dotnet:mainfrom
steveisok:fix/shared-memory-eexist-race

Conversation

@steveisok

@steveisoksteveisok commented Mar 13, 2026

Copy link
Copy Markdown
Member

Summary

Fix a TOCTOU (time-of-check-to-time-of-use) race condition in SharedMemoryHelpers.CreateOrOpenFile that causes intermittent IOException when two processes concurrently create a named mutex backed by shared memory on Linux.

The Bug

CreateOrOpenFile uses a two-step approach:

  1. Try Open(O_RDWR) to open an existing file
  2. If ENOENT, try Open(O_CREAT|O_EXCL) to create exclusively

The race:

  1. Process A: Open(O_RDWR) → ENOENT (file does not exist)
  2. Process B: creates the file successfully
  3. Process A: Open(O_CREAT|O_EXCL) → EEXIST (file now exists)
  4. Process A: throws IOException instead of handling EEXIST

This manifests as:

System.IO.IOException: The file '/tmp/.dotnet/shm/session1/NuGet-Migrations' already exists.
at System.IO.SharedMemoryHelpers.CreateOrOpenFile(...)
at System.Threading.Mutex..ctor(Boolean initiallyOwned, String name)
at NuGet.Common.Migrations.MigrationRunner.Run(...)

The Fix

When the exclusive create fails with EEXIST, retry from the top (loop back to the Open(O_RDWR) which will now succeed since the file exists). Limited to four retries to prevent infinite loops.

Impact

This is a known flaky failure in CI (labeled Known Build Error) that has been open since September 2023. It affects any scenario where parallel dotnet processes run first-time setup, including:

  • VMR scenario tests (ValidateInstallers on Linux arm64)
  • Docker container first-run
  • CI environments with shared /tmp

Fixes#91987
Related: #80619, #76736

When two processes concurrently create a named mutex backed by a shared
memory file, the following race can occur:
1. Process A calls Open(O_RDWR) — returns ENOENT (file doesn't exist)
2. Process B creates the file
3. Process A calls Open(O_CREAT|O_EXCL) — returns EEXIST (file now exists)
4. Process A throws IOException: 'The file already exists'
This manifests as intermittent IOException crashes in NuGet's
MigrationRunner when parallel dotnet processes run first-time setup,
because NuGet.Common.Migrations.MigrationRunner uses a named mutex
('NuGet-Migrations') to synchronize.
The fix adds a single retry: when the exclusive create fails with
EEXIST, loop back to re-attempt the plain open, which will now succeed
since the file exists.
Fixesdotnet#91987
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings March 13, 2026 15:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in SharedMemoryHelpers.CreateOrOpenFile on Unix by retrying when the exclusive create fails with EEXIST, preventing intermittent IOException when multiple processes concurrently create shared-memory-backed mutex files.

Changes:

  • Wrap the open/create sequence in a retry loop.
  • On O_CREAT|O_EXCL returning EEXIST, retry the initial O_RDWR open once.

You can also share your feedback on Copilot code review. Take the survey.

// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)
// and the exclusive create attempt (which may return EEXIST if another process created the file
// in between). On EEXIST, we loop back and re-attempt the open.
for (int retries = 0; ; retries++)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going need a bit of reworking, but I think it is the correct approach. It looks like we'd need to elevate the Interop.ErrorInfo out of the loop and after the loop body move the throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); that is currently being preempted by the new continue logic.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I figured the same thing. Felt it was good to push this up as a placeholder

Comment on lines +483 to +486
if (error.Error == Interop.Error.EEXIST && retries < 1)
{
continue;
}
Comment on lines +424 to +427
for (int retries = 0; ; retries++)
{
SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
if (!fd.IsInvalid)
{
if (id.IsUserScope)
// Retry loop to handle the TOCTOU race between the initial open attempt (which may return ENOENT)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A different idea to consider. Please take it with a grain of salt as I don't have the context:

  • when createIfNotExist is true, attempt in a loop until fd.IsValid:
    • create it with O_CREAT | O_EXCL. A success means we just created it and are the owner.
    • if creation failed with EEXIST, try to open it without creation
    • if opening without creation failed with ENOENT, it means it got removed in the meantime. Continue the loop
  • attempt to open an existing file

Pseudocode:

UnixFileModepermissionsMask=id.IsUserScope?PermissionsMask_OwnerUser_ReadWrite:PermissionsMask_AllUsers_ReadWrite;constInterop.Sys.OpenFlagsmandatoryFlags=Interop.Sys.OpenFlags.O_RDWR|Interop.Sys.OpenFlags.O_CLOEXEC
SafeFileHandle fd =new();while(createIfNotExist&&fd.IsInvalid){// Use O_EXCL which provides a guarantee that the file is created by this call.fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags|Interop.Sys.OpenFlags.O_CREAT|Interop.Sys.OpenFlags.O_EXCL,(int)permissionsMask);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();if(error.Error==Interop.Error.EEXIST){fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){error=Interop.Sys.GetLastErrorInfo();fd.Dispose();// The file could have been deleted after the first open attempt, in which case we should retry creating the file.if(error.Error==Interop.Error.ENOENT){continue;}}}throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}else{createdFile=true;}}if(fd.IsInvalid){Debug.Assert(!createIfNotExist);fd=Interop.Sys.Open(sharedMemoryFilePath,mandatoryFlags,0);if(fd.IsInvalid){Interop.ErrorInfoerror=Interop.Sys.GetLastErrorInfo();fd.Dispose();throwInterop.GetExceptionForIoErrno(error,sharedMemoryFilePath);}}// If we got here, fd is a valid file handle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this approach would work.

The main difference would be that we're optimizing (in terms of number of syscalls) for the create case here instead of the open existing case. Not that that really matters given all the other infrastructure here IMO.

@AaronRobinsonMSFTAaronRobinsonMSFTMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

@jkoritzinskyjkoritzinskyMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Adam's implementation, while a bigger diff from the existing code, is much easier to read and understand when looking at at a glance.

Adam's implementation also better follows Jared's advice (ie don't check for existence as existence may change. Just do the action and handle the errors when the file does not exist.
) https://blog.paranoidcoding.org/2009/12/10/the-file-system-is-unpredictable.html

@adamsitnikadamsitnikMar 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some minor modifications to the current one is easier to grep than this one. @adamsitnik what prompted your suggestions is there some optimization inherent in this alternative approach?

The problem we are trying to solve is very similar to some File.OpenHandle issues (and this is an API I own) and I personally prefer to use O_EXCL when dealing with TOCTOU because it's atomic.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is an API I own

Then we can absolutely go in that direction.

But again, as I wrote I don't have the context (is the most common case to create the file? or open an existing one?)

I don't have enough information to answer that either. I agree it does look like a pessimization with respect to existing paths, but as @jkoritzinsky points out there is a ton of machinery here so I'm not sure how much that matters.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

If we don't, we should add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any of these APIs in the perf lab? Is a there a microbenchmark we could run locally that would help see the cost?

@caaavik-msft@DrewScoggins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not ☹️

@jkoritzinsky

Copy link
Copy Markdown
Member

Really glad to see that moving the shared mutex logic to managed has helped us solve at least one bug!

Rework CreateOrOpenFile to use an atomic create-first approach instead of
open-then-create. This eliminates the TOCTOU race window by leading with
O_CREAT|O_EXCL and falling back to a plain open on EEXIST, with retries
for the reverse ENOENT case.
Add MutexTests to validate named mutex creation and concurrent access.
When another process creates the shared memory file, there is a small
window before it calls FChMod to set the correct permissions. If we
open the file during that window, ValidateExistingFile may fail due to
a permissions mismatch. Retry the outer loop in this case to give the
creator time to complete FChMod.
CopilotAI review requested due to automatic review settings March 17, 2026 20:23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race in Unix shared-memory file creation for named mutexes, preventing intermittent IOException under concurrent process creation on Linux/Unix.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile to handle EEXIST during exclusive create by falling back to open + retry logic.
  • Extracts user-scope file validation into ValidateExistingFile.
  • Adds a cross-process Unix test to validate concurrent named mutex creation does not throw.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

FileDescription
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements retry/fallback logic around exclusive create/open and factors validation into a helper.
src/libraries/System.Threading/tests/MutexTests.csAdds a RemoteExecutor-based regression test for concurrent named mutex creation on Unix.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +432 to +433
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
Comment on lines +429 to +438
// Lead with O_CREAT | O_EXCL for an atomic create guarantee. If another process
// created the file first (EEXIST), fall back to a plain open. If that open gets
// ENOENT (file was deleted in between), retry.
const int MaxRetries = 4;
for (int retries = 0; ; retries++)
{
if (Interop.Sys.FStat(fd, out Interop.Sys.FileStatus fileStatus) != 0)
SafeFileHandle fd = Interop.Sys.Open(
sharedMemoryFilePath,
MandatoryFlags | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL,
(int)permissionsMask);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. If we can benchmark, perhaps that would tell us if this is better.

You can use the @EgorBot to run the benchmarks via GH comment. An example: #125452 (comment)

It will run provided microbenchmark against a local build of dotnet/runtime and report the results as a comment.

Comment threadsrc/libraries/System.Threading/tests/MutexTests.cs
…istingFile
Narrow the EEXIST fallback retry to only catch transient permission
mismatches (UnauthorizedAccessException) instead of all IOExceptions.
UID mismatches are permanent and now propagate immediately without
a pointless retry.
Adjust the exception filter and ENOENT check to use MaxRetries - 1
so they are reachable on the last loop iteration. Without this, the
guards were dead code and loop exhaustion would silently fall through
to the open-existing path instead of throwing.
CopilotAI review requested due to automatic review settings March 18, 2026 12:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Linux TOCTOU race in Unix shared-memory backing files used by cross-process named mutexes, preventing intermittent IOException when multiple processes concurrently create the same mutex.

Changes:

  • Reworks SharedMemoryHelpers.CreateOrOpenFile (Unix) to handle EEXIST on exclusive create by retrying/opening, with bounded retries.
  • Adds a Unix cross-process regression test that concurrently creates the same named mutex across multiple RemoteExecutor processes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

FileDescription
src/libraries/System.Threading/tests/MutexTests.csAdds regression coverage for concurrent cross-process named mutex creation on Unix.
src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.csImplements bounded retry logic to close the create/open TOCTOU window and factors validation into a helper.

You can also share your feedback on Copilot code review. Take the survey.

if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite)
{
fd.Dispose();
throw new UnauthorizedAccessException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite));
@jkoritzinsky

Copy link
Copy Markdown
Member

@steveisok can we move forward on this PR?

// Exercises the TOCTOU race window in SharedMemoryHelpers.CreateOrOpenFile where
// one process sees ENOENT then another creates the file, causing the first to get
// EEXIST on the exclusive create. Multiple processes creating the same named mutex
// concurrently should succeed without IOException.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteExecutor.Invoke below is launched sequentially. This does not seem to exercise multiple processes creating the same named mutex concurrently.

if (!createIfNotExist)
{
createdFile = false;
ValidateExistingFile(fd, sharedMemoryFilePath, id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs the same retry loop as the other Interop.Sys.Open(sharedMemoryFilePath, MandatoryFlags, 0) call above

@steveisok

Copy link
Copy Markdown
MemberAuthor

Closing in favor of #129923

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The system cannot open the device or file specified 'NuGet-Migrations'

8 participants

@steveisok@jkoritzinsky@lewing@adamsitnik@jkotas@DrewScoggins@AaronRobinsonMSFT