From 0606c958bb8959893cb3f80f9028e428e8885e2e Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 18 Mar 2026 11:47:31 -0700 Subject: [PATCH 1/2] Add mount lock to prevent concurrent mount race condition Acquire a FileBasedLock on .gvfs/mount.lock at the start of InProcessMount.Mount() before any resource initialization or pipe creation. If a second GVFS.Mount.exe process starts for the same repo, it fails to acquire the lock and exits cleanly, allowing the first process to complete without conflict. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- GVFS/GVFS.Common/GVFSConstants.cs | 1 + GVFS/GVFS.Mount/InProcessMount.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/GVFS/GVFS.Common/GVFSConstants.cs b/GVFS/GVFS.Common/GVFSConstants.cs index cf52e1fbb..308ee91c6 100644 --- a/GVFS/GVFS.Common/GVFSConstants.cs +++ b/GVFS/GVFS.Common/GVFSConstants.cs @@ -116,6 +116,7 @@ public static class DotGVFS { public const string CorruptObjectsName = "CorruptObjects"; public const string LogName = "logs"; + public const string MountLock = "mount.lock"; public static class Databases { diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index c56627703..d1eeadb69 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -85,6 +85,24 @@ public void Mount(EventLevel verbosity, Keywords keywords) { this.currentState = MountState.Mounting; + string mountLockPath = Path.Combine(this.enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.MountLock); + using (FileBasedLock mountLock = GVFSPlatform.Instance.CreateFileBasedLock( + new PhysicalFileSystem(), + this.tracer, + mountLockPath)) + { + if (!mountLock.TryAcquireLock()) + { + this.tracer.RelatedInfo("Mount: Another mount process is already running. Exiting."); + return; + } + + this.MountWithLockAcquired(verbosity, keywords); + } + } + + private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) + { // Start auth + config query immediately — these are network-bound and don't // depend on repo metadata or cache paths. Every millisecond of network latency // we can overlap with local I/O is a win. From d4546ac3f1a47ac67ba533b16c10377c4142d251 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Mon, 23 Mar 2026 11:02:31 -0700 Subject: [PATCH 2/2] Address PR feedback: proper exit codes and error classification for mount lock - Add TryAcquireLock(out Exception) to FileBasedLock so callers can pattern-match on exception type to distinguish lock contention (IOException/sharing violation) from permission or I/O errors. - Replace bare return with FailMountAndExit() when lock acquisition fails, so the process exits with a non-zero exit code. - Add ReturnCode.MountAlreadyRunning (8) for the lock contention case, keeping GenericError (3) for unexpected lock failures. - Add FailMountAndExit(ReturnCode, ...) overload to support per-call exit codes while preserving existing callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- GVFS/GVFS.Common/FileBasedLock.cs | 18 +++++++++++++++++- GVFS/GVFS.Common/ReturnCode.cs | 1 + GVFS/GVFS.Mount/InProcessMount.cs | 17 +++++++++++++---- .../WindowsFileBasedLock.cs | 8 ++++++-- .../Mock/Common/MockFileBasedLock.cs | 4 +++- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/GVFS/GVFS.Common/FileBasedLock.cs b/GVFS/GVFS.Common/FileBasedLock.cs index 9f709f2cf..e87fc7eb6 100644 --- a/GVFS/GVFS.Common/FileBasedLock.cs +++ b/GVFS/GVFS.Common/FileBasedLock.cs @@ -20,7 +20,23 @@ public FileBasedLock( protected string LockPath { get; } protected ITracer Tracer { get; } - public abstract bool TryAcquireLock(); + public bool TryAcquireLock() + { + return this.TryAcquireLock(out _); + } + + /// + /// Attempts to acquire the lock, providing the exception that prevented acquisition. + /// + /// + /// When the method returns false, contains the exception that prevented lock acquisition. + /// Callers can pattern-match on the exception type to distinguish lock contention + /// (e.g. with a sharing violation HResult) from + /// permission errors () or other failures. + /// Null when the method returns true. + /// + /// True if the lock was acquired, false otherwise. + public abstract bool TryAcquireLock(out Exception lockException); public abstract void Dispose(); } diff --git a/GVFS/GVFS.Common/ReturnCode.cs b/GVFS/GVFS.Common/ReturnCode.cs index f99f3875d..5243cb2f5 100644 --- a/GVFS/GVFS.Common/ReturnCode.cs +++ b/GVFS/GVFS.Common/ReturnCode.cs @@ -10,5 +10,6 @@ public enum ReturnCode NullRequestData = 5, UnableToRegisterForOfflineIO = 6, DehydrateFolderFailures = 7, + MountAlreadyRunning = 8, } } diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index d1eeadb69..cdfa76307 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -91,10 +91,14 @@ public void Mount(EventLevel verbosity, Keywords keywords) this.tracer, mountLockPath)) { - if (!mountLock.TryAcquireLock()) + if (!mountLock.TryAcquireLock(out Exception lockException)) { - this.tracer.RelatedInfo("Mount: Another mount process is already running. Exiting."); - return; + if (lockException is IOException) + { + this.FailMountAndExit(ReturnCode.MountAlreadyRunning, "Mount: Another mount process is already running."); + } + + this.FailMountAndExit("Mount: Failed to acquire mount lock: {0}", lockException.Message); } this.MountWithLockAcquired(verbosity, keywords); @@ -314,6 +318,11 @@ private NamedPipeServer StartNamedPipe() } private void FailMountAndExit(string error, params object[] args) + { + this.FailMountAndExit(ReturnCode.GenericError, error, args); + } + + private void FailMountAndExit(ReturnCode returnCode, string error, params object[] args) { this.currentState = MountState.MountFailed; @@ -330,7 +339,7 @@ private void FailMountAndExit(string error, params object[] args) this.fileSystemCallbacks = null; } - Environment.Exit((int)ReturnCode.GenericError); + Environment.Exit((int)returnCode); } private T CreateOrReportAndExit(Func factory, string reportMessage) diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileBasedLock.cs b/GVFS/GVFS.Platform.Windows/WindowsFileBasedLock.cs index a965304e4..edf1c43a0 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileBasedLock.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileBasedLock.cs @@ -36,8 +36,9 @@ public WindowsFileBasedLock( { } - public override bool TryAcquireLock() + public override bool TryAcquireLock(out Exception lockException) { + lockException = null; try { lock (this.deleteOnCloseStreamLock) @@ -63,13 +64,14 @@ public override bool TryAcquireLock() catch (IOException e) { // HResultErrorFileExists is expected when the lock file exists - // HResultErrorSharingViolation is expected when the lock file exists andanother GVFS process has acquired the lock file + // HResultErrorSharingViolation is expected when the lock file exists and another GVFS process has acquired the lock file if (e.HResult != HResultErrorFileExists && e.HResult != HResultErrorSharingViolation) { EventMetadata metadata = this.CreateLockMetadata(e); this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: IOException caught while trying to acquire lock"); } + lockException = e; this.DisposeStream(); return false; } @@ -78,6 +80,7 @@ public override bool TryAcquireLock() EventMetadata metadata = this.CreateLockMetadata(e); this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: UnauthorizedAccessException caught while trying to acquire lock"); + lockException = e; this.DisposeStream(); return false; } @@ -86,6 +89,7 @@ public override bool TryAcquireLock() EventMetadata metadata = this.CreateLockMetadata(e); this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: Win32Exception caught while trying to acquire lock"); + lockException = e; this.DisposeStream(); return false; } diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockFileBasedLock.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockFileBasedLock.cs index c18c707b4..ba821d1d5 100644 --- a/GVFS/GVFS.UnitTests/Mock/Common/MockFileBasedLock.cs +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockFileBasedLock.cs @@ -1,6 +1,7 @@ using GVFS.Common; using GVFS.Common.FileSystem; using GVFS.Common.Tracing; +using System; namespace GVFS.UnitTests.Mock.Common { @@ -14,8 +15,9 @@ public MockFileBasedLock( { } - public override bool TryAcquireLock() + public override bool TryAcquireLock(out Exception lockException) { + lockException = null; return true; }