Release M277.2#2067
Merged
Merged
Conversation
git invokes core.virtualfilesystem via the shell (use_shell=1 in virtualfilesystem.c), so on Windows the shell has to probe PATHEXT candidates (.COM, .EXE, ...) to resolve the bare "virtual-filesystem" path to the actual "virtual-filesystem.exe" file that HooksInstaller writes to disk. Since VFSForGit controls both the config value and the file it points to, append GVFSPlatform.Instance.Constants.ExecutableExtension so the path matches the file exactly, avoiding repeated PATHEXT probing on every hook invocation. This is a no-op on platforms where ExecutableExtension is empty (macOS/Linux). Assisted-by: Claude Sonnet 5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Include executable extension in core.virtualfilesystem path
The greedy bucket slicer keeps consecutive tests from the same fixture
class together when they share state. The grouping regex was overly
broad, using whole namespaces instead of specific class names:
OLD: ^.*\.(?:EnlistmentPerFixture|MultiEnlistmentTests)\..+\.
NEW: ^.*\.(?:EnlistmentPerFixture\..+|MultiEnlistmentTests\.ConfigVerbTests)\.
Two problems fixed:
1. GitCommands namespace (previous PR run): included all GitCommands
tests, causing all 38 CheckoutTests(Full) methods (~16 min) to land
in one slice. GitCommands tests are safe to split: they use either a
per-test enlistment or a git-checkout reset in [SetUp].
2. MultiEnlistmentTests namespace (this change): included all classes
in the namespace, keeping SharedCacheTests (10 tests, ~7 min) and
ServiceVerbTests together unnecessarily.
- SharedCacheTests: [SetUp] generates a Guid-based cache path per
test — no shared state across test methods. Safe to split.
- ServiceVerbTests: [NonParallelizable] blocks within-process
concurrency; different slices run on different machines. Safe.
- ConfigVerbTests: uses [Order(1..8)] with each test reading config
written by the previous. Must stay together — kept explicitly.
- EnlistmentPerFixture: still grouped per-class as before.
From PR #2028 baseline: critical path was 18.9 min.
After GitCommands fix: 9.85 min.
Expected after this change: ~6-7 min.
Assisted-by: Claude Sonnet 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
fix: tighten FT slicer grouping regex to class-level
….vfs.0.4 Update default Microsoft Git version to v2.54.0.vfs.0.4
Under Windows "Administrator protection" (AP), an elevated process runs as a hidden, profile-separated shadow admin account (MACHINE\admin_<user>) whose SID is a local account (S-1-5-21-...) that differs from the real (non-elevated) user's SID. When `gvfs clone` runs elevated, the enlistment's `src` and `.git` directories are created owned by the Administrators group. EnsureDirectoryIsOwnedByCurrentUser then reassigned ownership to the current user to satisfy git's dubious-ownership check. Under AP the "current user" is the shadow admin, so ownership was set to admin_<user> — which the real user does not match. The real (non-elevated) user then hit `fatal: detected dubious ownership`, because git's Administrators-group membership grace does not cover another specific user's SID, and the shadow admin cannot SetOwner to the real user's SID either. Detect the AP shadow admin from the running process's own token — its effective elevation identity — and, when present, leave the directory owner as the Administrators group instead of reassigning it to the shadow admin. An Administrators-owned directory is accepted by modern git and the libgit2 non-elevated-admin-owner overlay for any Administrators member — the real user, the shadow admin, and SYSTEM (for automount) alike. Detection inspects the token rather than the TypeOfAdminApprovalMode policy value so it stays correct across an AP policy change that has not yet been rebooted (where the policy value is pending but elevation still yields a shadow admin). A shadow admin is identified as a local account SID (S-1-5-21-...) whose ProfileList profile directory is ADMIN_-prefixed; the real interactive user (a domain or Entra ID account) is excluded. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The `non-elevated-admin-owner.diff` overlay patch was copied verbatim from libgit2 PR #7200. That PR version had four bugs that maintainers fixed before merging to libgit2 `main`: - `linked_token` HANDLE from TokenLinkedToken was never CloseHandle'd, leaking a handle in the long-lived GVFS mount service (SYSTEM). - `current_user_sid` used `static HANDLE linked_token` — not thread-safe and retained a stale handle across calls; replaced with a per-call local initialized to NULL by the caller. - The TokenLinkedToken failure path nulled the local pointer parameter (`linked_token = NULL`) instead of `*linked_token = NULL` (+CloseHandle). - `current_user_sid()` was only invoked in the CURRENT_USER branch, but the admin-membership branch also reads `linked_token`; a caller passing only USER_IS_ADMINISTRATOR would read an uninitialized handle. It is now called unconditionally before either branch. Regenerate the patch so that, applied to the pinned libgit2 source, it produces the same corrected `src/util/fs_path.c` as upstream `main` (commits cc477ee + f9f36a6 + 44c05e5, merge e805a16, merged 2026-06-07). The patch is not yet in any libgit2 release, so the overlay remains necessary. Also bump the pinned libgit2 version 1.9.3 -> 1.9.4 (matching the official vcpkg port). fs_path.c is byte-identical between 1.9.3 and 1.9.4, so the patched region is unaffected and the diff applies cleanly to both. Verified: Build.bat Debug rebuilds libgit2 for both x64-windows-static-aot and x64-windows-dynamic triplets, applying non-elevated-admin-owner.diff with no hunk failures, and the full solution builds to an installer. Complements #2039 (the GVFS-side AP clone-ownership fix). Work item: AB#62918022. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Fix clone ownership under Windows Administrator Protection
…ner-overlay Re-sync libgit2 overlay admin-owner patch with merged upstream (bump to 1.9.4)
GVFS.Mount could crash with OutOfMemoryException. GitProcess.InvokeGitImpl
buffered all of a git process's stdout and stderr into unbounded StringBuilders
via the DataReceived handlers. A corrupt or truncated packfile in the shared
object cache makes 'git multi-pack-index' stream a large volume of "could not
load pack" errors to stderr, growing the buffer until the mount OOM-crashes.
Bound each captured stream with its own cap:
- stderr: ~20 MB (10M chars). stderr is diagnostic, so truncating it is safe;
this is the cap that stops the corrupt-packfile crash.
- stdout: ~256 MB (128M chars) when the caller buffers the whole result.
Sized to hold the machine-readable (-z) output of the largest real
repositories - on the order of 2.8M status/diff records on the biggest
os.2020 branches - with headroom, while staying far below the .NET maximum
array/string size so the buffer itself cannot OOM. Commands that stream
their output line-by-line are unaffected.
Truncation is surfaced on GitProcess.Result via OutputTruncated and
ErrorsTruncated so callers can react:
- stdout carries correctness-critical data for two commands, which now fail
safe on truncation rather than act on a partial result:
- AddStagedFilesToModifiedPaths (git diff --cached --name-status -z):
refuses to update ModifiedPaths from a partial staged-file list, which
would otherwise leave staged files with skip-worktree set and stale
placeholders.
- SparseVerb's git status check (status --porcelain -z): aborts sparse
rather than risk proceeding over uncommitted changes it failed to see.
Both emit telemetry on truncation.
- stderr truncation is expected and non-fatal; GitMaintenanceStep.RunGitCommand
logs it for diagnostics but does not fail the command for it.
This is the first of a series: a follow-up converts those two -z commands to
stream their output (removing the truncation exposure entirely), and packfile
corruption auto-recovery is a separate change.
Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
SQLITE_IOERR is excluded from the retry-eligible set because it can fire during connection disposal *after* a write has already committed to the WAL. In ExecuteReadThenWrite (used by RemoveAllEntriesForFolder), this produces a stale empty result on retry: the SELECT finds zero rows (DELETE already committed), so the returned list of removed placeholders is wrong. If the dehydrate operation subsequently fails, the rollback loop re-adds nothing and placeholder entries are silently lost. BUSY and LOCKED are safe to retry: both mean the operation was blocked before executing and was never committed. SQLite guarantees this. IOERR on a genuinely failed write is also rolled back atomically by SQLite, but we cannot distinguish a post-commit IOERR (from Dispose) from a pre-commit IOERR (from ExecuteNonQuery) at the catch site. The safe choice is to surface IOERR immediately and let the caller decide. The root cause of the production LOCKED errors (#2031) was Cache=Shared, which is already removed. IOERR tolerance was speculative; removing it eliminates a real (if rare) correctness hazard with no practical loss. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #1980 made TryAttachToVolume tolerate ACCESS_DENIED from FilterAttach whenever the ProjFS service is running. Because both callers share this method, the elevated GVFS.Service (running as LocalSystem) also began swallowing ACCESS_DENIED — but the service holds SE_LOAD_DRIVER_PRIVILEGE, so ACCESS_DENIED there is never the benign "unprivileged caller" case. It signals a real problem (AV/EDR lock, Group Policy, or driver corruption), and tolerating it lets a broken mount proceed instead of surfacing an actionable error. Before #1980 the service called raw TryAttach and reported that failure. FilterAttach's ACCESS_DENIED is a privilege gate, not a transient contention error: an unelevated caller always gets it (regardless of the actual attach state), while an elevated caller only gets it when something is genuinely wrong. Narrow the tolerance to unelevated callers so the unelevated `gvfs mount` fix is preserved while the elevated service once again surfaces genuine attach failures. This also aligns the code with the existing comment's stated intent ("when the caller lacks this privilege"). Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Change the outer try-catch in HandleRequest from swallowing exceptions to catch-log-rethrow. The original catch (PR #2021) suppressed all exceptions except OutOfMemoryException, which could mask corruption in state-mutating handlers like PostIndexChanged, DehydrateFolders, and ReleaseLock — leaving the mount serving in an inconsistent state. The catch now logs the message header and exception (context that OnNewConnection's generic catch in NamedPipeServer does not capture), then rethrows so fail-fast via Environment.Exit still occurs for unhandled errors in state-mutating handlers. The inner try-catch in HandleDownloadObjectRequest remains unchanged — it correctly catches and returns DownloadFailed for transient network/disk errors in the download path, which is safe because no critical mount state is mutated. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #2005 restructured mount startup so the named pipe server starts before the parallel auth+validation wait. That early-pipe move is a reliability improvement -- it widens the WaitUntilMounted connect window, avoiding "GVFS.Mount process is not responding" failures when auth is slow (including the SYSTEM-service automount path) -- and is separable from the progress-display feature layered on top of it. Add a disabled-by-default gvfs.mount-progress config flag that gates only the display layer. When off, the mount process no longer surfaces MountProgress phase strings over the named pipe, and the CLI falls back to its existing static spinner (ConsoleHelper already renders just the base message when progress is empty). The reliability infrastructure -- early pipe start, HandleRequest Mounting-state guard, volatile currentState, and null-safe HandleGetStatusRequest -- is always on regardless of the flag, so stabilization builds keep the automount robustness without shipping the newer progress-display behavior. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…tion Add two sections to AGENTS.md capturing repo facts that routinely mislead agents and aren't obvious from a fresh clone: - "What ships in a public release": FastFetch is built/signed but NOT attached to the GitHub release, and Git is not bundled — "default Microsoft Git version" bumps only change CI GIT_VERSION, while the shipped floor is MinimumGitVersion in Version.props. Both matter when scoping a release or writing changelog notes. - "Feature flags": product flags are git config keys under the gvfs. prefix, declared in GVFSConstants.cs and read via GetConfigBoolOrDefault, mirroring gvfs.show-hydration-status. Gate the runtime entry point, default false. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…t flag ProjFS does not always deliver EndDirectoryEnumeration for a StartDirectoryEnumeration (for example when an enumeration is cancelled), which leaks the corresponding ActiveEnumeration - and the projected item list it pins - in this.activeEnumerations. Over long-lived GVFS.Mount processes this unbounded growth is a suspected contributor to the memory pressure behind the native ProjFS command-completion crashes that surface downstream as STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE (0xC000CE01). "Suspected" is the operative word: Watson shows radar_high_memory in projectedfslib.dll!prjcompletecommand, but we do not yet have direct evidence that the never-ended-enumeration leak is a dominant driver of that memory. This change is therefore split into two parts so we can measure before we change the enumeration hot path anywhere: 1. Observation telemetry (always on). ActiveEnumeration records a monotonic LastActivityTickCount (Environment.TickCount64) on creation and on every GetDirectoryEnumeration read. WindowsFileSystemVirtualizer now contributes ActiveEnumerationCount and ActiveCommandCount to the periodic Heartbeat event via a new FileSystemVirtualizer.AddHeartbeatMetadata hook. This ships the signal needed to confirm (or refute) that activeEnumerations grows without bound on real machines - independent of whether eviction is enabled. 2. Eviction (off by default). When the gvfs.max-active-enumerations git config is set to a positive value, a throttled sweep (at most once per minute, and only once the collection exceeds the configured count) evicts enumerations idle longer than a 5 minute timeout. Config unset or <= 0 disables eviction entirely, so the enumeration hot path is unchanged by default. Monotonic clocks are used for both the throttle and the staleness cutoff so wall-clock adjustments cannot disturb them, and the throttle claim uses an Interlocked.CompareExchange keyed on the previously-read tick so two threads entering the same interval cannot both sweep. Evicting a live-but-idle enumeration is safe: the next GetDirectoryEnumeration for that id misses activeEnumerations and returns HResult.InternalError - it fails that one directory listing loudly rather than returning truncated results as if complete - and every eviction is reported via telemetry. This complements the stabilization-safe "do not crash the mount on a native ProjFS failure" change (#2042), which is intentionally kept separate and does not touch the enumeration hot path. Eviction stays behind the flag until the Heartbeat telemetry confirms the leak is worth acting on. Tests: ActiveEnumeration records activity time; Heartbeat metadata reports the live enumeration count; with eviction disabled stale enumerations are retained; with eviction enabled stale enumerations are evicted (and their subsequent End fails with InternalError) while freshly-active ones are kept. Full unit suite passes. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The in-product org auto-upgrade mechanism (OrgInfo server + upgrade ring + NuGet feed) has been dead for years: `gvfs upgrade` is already a no-op stub and nothing consumes these paths. Fleet upgrades are driven externally from GitHub Releases. Remove the dead code while keeping the `gvfs upgrade` verb in its current no-op stub state: - OrgInfoApiClient / VersionResponse (and its GVFSJsonContext registration) - AzDevOpsOrgFromNuGetFeed (NuGet-feed org-parse helper, only referenced by the dead upgrader path and its own test) - LocalGVFSConfig keys upgrade.ring / upgrade.feedpackagename / upgrade.feedurl / upgrade.orgInfoServerUrl - the now-unreferenced GVFSConstants.UpgradeVerbMessages class - MockLocalGVFSConfigBuilder and the removed types' unit tests - installer ring->NuGet-feed plumbing (SetNuGetFeedIfNecessary, GetConfiguredUpgradeRing, IsConfigured, SetIfNotConfigured, UpgradeRing) Build (including the Inno Setup installer) and all unit tests pass. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Bound git stdout/stderr capture to prevent GVFS.Mount OOM
Narrow IsTransientError to BUSY+LOCKED only; exclude IOERR
…e (0xC000CE01) GVFS.Mount crashes are being reported that manifest downstream as STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE (0xC000CE01): once the mount process dies, the ProjFS virtualization root is orphaned and every subsequent placeholder access fails (for example GetOfficialBranch/libgit2 reading the virtualized .git). Watson attributes the crashes to radar_high_memory projectedfslib.dll!prjcompletecommand, with the dominant managed signatures being NullReferenceException surfaced from the native ProjFS command-completion (PrjCompleteCommand) and delete (PrjDeleteFile) paths under memory pressure. VFS for Git is already on the latest Microsoft.Windows.ProjFS package, so the native projectedfslib.dll fault cannot be fixed here directly. This is not a v2.0 regression: the unguarded call sites date to the 2018 .NET Framework era. The v2.0 pure-C# ProjFS rewrite made the same latent native fault legible as a managed NRE in GVFS telemetry, which is why reports increased recently. Do not crash the mount on a native ProjFS failure. A single boundary helper, TryInvokeProjFS, wraps each GVFS-initiated native ProjFS call: it catches the exception, emits high-signal *_NativeFailure telemetry (including live enumeration/command counts and GC memory), and returns HResult.InternalError so the caller maps it to a failure result and keeps serving the mount. Routed through it: CompleteCommand (via TryCompleteCommand, covering the completion leg of all async callbacks), DeleteFile, WritePlaceholderFile, WritePlaceholderDirectory, UpdatePlaceholderIfNeeded, and ClearNegativePathCache. MarkDirectoryAsPlaceholder mutates projection state, so swallowing is unsafe; it uses a sibling helper InvokeProjFSOrThrow that emits the same telemetry and then rethrows, preserving fail-fast. GetFileStreamHandlerAsyncHandler already fails a single hydration on any exception, so it is left as-is. When a native CompleteCommand fault is swallowed for a directory-enumeration start, ProjFS never accepted the completion and will not send the matching EndDirectoryEnumeration callback. TryCompleteCommand now reports the command as not completed in that case, so StartDirectoryEnumerationAsyncHandler removes the ActiveEnumeration it just registered instead of leaking it. A companion change to bound the activeEnumerations leak (a suspected contributor to the memory pressure) is deliberately kept out of this stabilization-safe fix and tracked separately behind an off-by-default config flag, so that the enumeration hot path is not altered until telemetry confirms the leak is a real driver. Tests: DeleteFile and the other outbound operations return IOError when the virtualization instance throws; a directory-enumeration start whose native completion faults removes its ActiveEnumeration rather than leaking it. Full unit suite passes. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
`gvfs service --unmount-all` intermittently failed with exit code 3 and "Already unmounting, please wait" when the service registry still listed a repo whose GVFS.Mount process was in the transient Unmounting state (a concurrent or prior unmount still shutting it down). The single IsRepoMounted helper returned true whenever the mount pipe merely answered a GetStatus request, regardless of the actual MountState, so --unmount-all would attempt to unmount a repo already on its way out and treat that transient state as a hard failure. Replace IsRepoMounted with three intent-specific predicates built on a TryGetRepoMountStatus primitive that reads the actual MountState: - IsRepoReady (Ready) -> --list-mounted - IsRepoAvailableToMount (no live process) -> --mount-all - IsRepoAvailableToUnmount (Ready|MountFailed) -> --unmount-all --unmount-all now skips repos that are Unmounting (already reaching the desired state) or Mounting (an unmount request would be rejected anyway), eliminating the spurious failure. --list-mounted now reports only fully-Ready repos. --mount-all behavior is unchanged (it still mounts only when no live mount process is answering). StatusVerb exposes the "Mount status: " output prefix as a shared constant so ServiceVerb recovers the MountState without re-implementing the pipe protocol. Fixes the flaky GVFS.FunctionalTests.Tests.MultiEnlistmentTests.ServiceVerbTests.ServiceCommandsWithNoRepos. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
PR #1989 fixed a PID-recycling race in orphan-lock detection by capturing the holder's process start time at acquire and comparing it at each orphan check. That fix is correct for the recycling case, but it collapsed every failure to read the start time into a single bool "false" that the caller treats as "the holder is gone" and releases the lock. The dangerous consequence is on the transient-failure path. The Windows implementation returns false the moment OpenProcess(QUERY_LIMITED_INFORMATION) yields an invalid handle. Under momentary resource pressure that OpenProcess can fail for a process that is genuinely still alive, so the orphan check can release a lock that is still legitimately held -- admitting a second writer to the index. That is a worse failure class (potential corruption) than the flaky hang #1989 set out to fix, which is exactly the kind of regression a stabilization release must not ship. (Note the pre-#1989 code did not have this exposure: its liveness check fell back to Process.GetProcessById when OpenProcess failed; #1989 dropped that fallback on the identity path.) This change makes the release decision reason-aware so that a lock is only released on positive evidence that the holder is gone: * TryGetActiveProcessStartTime now returns a ProcessStartTimeResult enum (Success / ProcessNotFound / Inaccessible / Indeterminate) instead of bool. The Windows implementation classifies an invalid OpenProcess handle by the Win32 error: ERROR_INVALID_PARAMETER -> ProcessNotFound (no such PID), ERROR_ACCESS_DENIED -> Inaccessible, anything else (e.g. ERROR_NOT_ENOUGH_MEMORY / ERROR_NO_SYSTEM_RESOURCES) -> Indeterminate. An opened-but-exited process (exit code != STILL_ACTIVE) maps to ProcessNotFound. * The orphan check in GVFSLock releases the lock only for: - Success with a different start time (PID recycled -- the #1989 case) - ProcessNotFound (positive: holder is gone) - Inaccessible (see gate argument below) and deliberately KEEPS the lock for Indeterminate, letting the existing 250 ms wait-loop poll re-evaluate. A transient read failure can no longer release a live holder's lock. * Inaccessible is safe to treat as "holder gone" because of an acquire-time gate: we only enter the identity-check path for a holder whose start time we successfully read at acquire, i.e. one this mount could open. OpenProcess access is a stable function of the caller token and the target's protection level / DACL (git and the hooks never rewrite their own DACL), so a live original holder we could open before cannot later become inaccessible. An access-denied result therefore means the PID now refers to a different process, so releasing is correct. The pre-existing null-start-time fallback (used when we could not read the start time at acquire) is unchanged. Telemetry: the Indeterminate hold emits an ExternalHolderLivenessIndeterminate event so we can measure whether these transient failures ever occur in the field. This is in-memory mount-side state only; the named-pipe lock protocol is unchanged, so there is no cross-version or wire-format impact. Tests: adds unit coverage for the new outcomes -- ProcessNotFound and Inaccessible each release the orphaned lock, and Indeterminate keeps it (the anti-false-release guard). Existing PID-recycle and start-time-match tests are retained. Full GVFS.UnitTests suite passes (885 passed, 0 failed). Assisted-by: Claude Opus 4.7 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Run ServiceVerbTests against a dedicated GVFS service name so --mount-all/--unmount-all only touch repos created in that fixture. This wires service-name overrides through functional-test helpers and adds fixture-local service lifecycle setup/teardown for ServiceVerbTests. Assisted-by: GPT-5.3-Codex Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
HealthTests read placeholder/modified-path counts via 'gvfs health' immediately after hydrating files via File.OpenWrite(). Those writes trigger ProjFS callbacks that GVFS processes on a background queue, so the modified-paths database may not yet reflect the full set of transitions when the health verb samples it. The resulting race manifests as: fast-file count is still 5 (some Scripts/*.bat entries still counted as placeholders) when the test expects 3 after TurnPlaceholdersIntoModifiedPaths. Fix: call WaitForBackgroundOperations() in ValidateHealthOutputValues before invoking the health verb, consistent with every other functional test that asserts modified-path state after a file operation (e.g. GitFilesTests, ModifiedPathsTests, CheckoutTests). Assisted-by: Claude Sonnet 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
When a cache server is configured locally, mount starts virtualization without waiting for the background auth/config query (PR #2034). During that window, TryInitializeAndQueryGVFSConfig has already published IsAnonymous=false but has not yet set isInitialized (the credential fetch runs in between). A directory enumeration that arrives in this window flows through HttpRequestor.SendRequest -> GitAuthentication.TryGetCredentials, which threw InvalidOperationException("must be initialized"). Thrown into the ProjFS StartDirectoryEnumerationAsyncHandler callback, this crashed the mount process: StartDirectoryEnumerationAsyncHandler caught unhandled exception, exiting process Fix: instead of throwing when called before initialization, TryGetCredentials now waits (bounded by InitializationWaitTimeoutMs, defaulting to the background credential timeout) for initialization to complete, then proceeds normally. If initialization never completes within the timeout it returns a retryable failure rather than crashing. All initialization terminal paths signal a ManualResetEventSlim via a new MarkInitialized() helper. This is isolated to GitAuthentication.cs so it can land independently of the separate change that will gate the background cache-auth behavior behind a config flag. Adds deterministic unit tests that reproduce the race: reintroducing the throw fails them; the fix makes them pass. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…-auth PR #2034 changed mount so that, when a cache server URL is already configured in local git config, authentication and the /gvfs/config query run as a fire-and-forget background task instead of blocking mount startup. As part of GVFS 2.0 stabilization we want that behavioral change to be off by default and opt-in, while keeping the associated diagnostic/reliability improvements (the distinct startup exit codes, the bounded credential timeout, and the cache-server fallback) always on. This adds a git-config flag, gvfs.background-cache-auth (default false), that gates only the fire-and-forget behavior: - Flag off (default): mount blocks on auth + /gvfs/config synchronously before starting virtualization, even when a cache server is configured. Because the network task is awaited, the fetched config is used and a synchronous (Default) credential timeout applies. - Flag on: the #2034 background behavior — mount proceeds without waiting on auth, using the local cache server URL, with the longer background credential timeout. The gate is a single derived condition, useBackgroundAuth = hasCacheServer && flag, applied to the wait decision, the credential-timeout selection, and the source of serverGVFSConfig. The pre-existing cache-server fallback, the startup exit codes, and ValidateGVFSVersion leniency remain keyed on hasCacheServer and are unchanged. The flag is read via libgit2 in-process (a short-lived LibGit2Repo, since the GVFSContext is not created until later in mount), defaulting to off on any failure. Isolated to GVFSConstants.cs and InProcessMount.cs; does not touch GitAuthentication.cs, so it does not conflict with the separate fix that hardens TryGetCredentials against being called before initialization completes. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Mount: gate background cache-server auth behind gvfs.background-cache-auth
Mount: don't crash when auth is used before background init completes
HealthTests: wait for background ops before gvfs health
Harden orphan-lock detection against false-release on transient failures
…n-leak Bound leaked ActiveEnumeration collection behind an off-by-default flag + Heartbeat observation telemetry
…status Split ServiceVerb repo-state check into intent-specific predicates
Remove dead org auto-upgrade code
Default gvfs.prefetch-offload to false and skip mount offload runtime paths unless enabled. Add heartbeat FeatureFlags.PrefetchOffload telemetry and unit assertions. Assisted-by: GPT-5.3-Codex Copilot-Session: 85dced45-8638-4b33-a1d1-8af4c8e37fb0 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The gvfs.prefetch-offload flag state is already captured fleet-wide by the git Trace2 collector via trace2.configparams (scalar.*,gvfs.*,...), surfaced as Summary.payload.params. Re-emitting it in VFS.Heartbeat is redundant and client-side telemetry adopts slowly across the GVFS fleet. Drop the FeatureFlags heartbeat metadata and the GitRepo.GetConfigBoolOrDefault wrapper added only to feed it. The gvfs.prefetch-offload gate in PrefetchVerb is unaffected. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Scope ServiceVerbTests to a fixture-local GVFS service
Gate prefetch offload behind gvfs.prefetch-offload
Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)
docs(agents): document release-shipping scope and feature-flag convention
…-gate ProjFS: only tolerate FilterAttach ACCESS_DENIED for unelevated callers
Mount: remove broad HandleRequest exception catch
Gate CLI mount-progress display behind gvfs.mount-progress config
KeithIsSleeping
approved these changes
Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes: