Skip to content

Bound runtime credential fetch so prefetch can't hang on auth#2046

Draft
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/prefetch-auth-timeout
Draft

Bound runtime credential fetch so prefetch can't hang on auth#2046
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/prefetch-auth-timeout

Conversation

@tyrielv

@tyrielv tyrielv commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

A user missed a Git Credential Manager auth popup (it was behind another window). Instead of timing out, GVFS waited indefinitely. The blocked operation was the mount's background maintenance PrefetchStep (not user-initiated), which holds the shared prefetch-commits-trees.lock on the gvfs object cache. Because that lock was never released, a subsequent user-initiated gvfs prefetch blocked behind it forever.

Root cause

Runtime credential fetches flow through:

HttpRequestor.SendRequest
  -> GitAuthentication.TryGetCredentials(tracer, out cred, out err)
  -> GitAuthentication.TryCallGitCredential(tracer, out err)   // timeoutMs defaulted to -1
  -> GitProcess.TryGetCredential(..., timeoutMs: -1)
  -> InvokeGitImpl(... timeoutMs: -1) -> Process.WaitForExit(-1)  // infinite

The mount startup auth path was already bounded (credentialTimeoutMs on TryInitializeAndQueryGVFSConfig), but the runtime TryGetCredentials path — used by every object/pack download, including the background maintenance prefetch — was never given a finite timeout. The timeout plumbing already exists end-to-end in GitProcess/InvokeGitImpl; the runtime path simply never passed a finite value.

The mount's maintenance prefetch and on-demand hydration share one GitObjectsHttpRequestor / one GitAuthentication, so bounding the shared runtime path fixes the maintenance-prefetch hang.

Fix

Bound the runtime credential fetch so it can never wait forever:

  • GitAuthentication.TryGetCredentials gains a trailing int credentialTimeoutMs = DefaultCredentialTimeoutMs and passes it to TryCallGitCredential.
  • GitAuthentication.RejectCredentials — which reloads the credential on the 401-retry leg (SendRequest calls it before re-entering TryGetCredentials) — takes and plumbs the same timeout. This leg is the actual stale-token hang path and was otherwise still unbounded.
  • HttpRequestor adds a protected virtual int CredentialTimeoutMs => GitAuthentication.BackgroundCredentialTimeoutMs (120s) and passes it to both TryGetCredentials and RejectCredentials in SendRequest.

Why 120s and not 30s: the mount's requestor is shared by the background maintenance prefetch, interactive on-demand hydration, and the user-initiated gvfs prefetch/clone verbs. A 30s bound risks converting today's hang into a spurious auth failure when a human is legitimately slow to answer a GCM cold-start / MFA / smartcard prompt. 120s (the existing, already-vetted BackgroundCredentialTimeoutMs) still bounds the indefinite hang — the reported case was a prompt never answered — while giving a noticed prompt ample time.

On timeout, TryGetCredentials returns false and engages backoff; after the second failed attempt IsBackingOff short-circuits further attempts. The download gives up, TryDownloadPrefetchPacks returns false, and PrefetchStep releases prefetch-commits-trees.lock — unblocking the user-initiated prefetch — instead of hanging forever.

All existing callers compile unchanged (new parameter has a default; property is virtual with a default).

Tests

  • TryGetCredentialsTimesOutWhenCredentialManagerDoesNotRespond — simulates a git credential fill timeout and asserts TryGetCredentials returns false with a "did not respond" error.
  • TryGetCredentialsSucceedsWithExplicitTimeout — happy path with an explicit finite timeout.
  • Full GitAuthenticationTests class: 22/22 passing; GVFS.UnitTests builds with 0 warnings / 0 errors.

Fixes AB#63011829

@tyrielv tyrielv force-pushed the tyrielv/prefetch-auth-timeout branch 2 times, most recently from 6223f5a to 12eb591 Compare July 10, 2026 17:37
The runtime credential path (HttpRequestor.SendRequest ->
GitAuthentication.TryGetCredentials/RejectCredentials ->
TryCallGitCredential) called git-credential with timeoutMs = -1, so
Process.WaitForExit(-1) waited forever. When a GCM auth popup was missed
(e.g. behind another window), the mount's background maintenance
PrefetchStep blocked indefinitely while holding the shared
prefetch-commits-trees.lock, which in turn blocked a user-initiated
`gvfs prefetch`.

The mount startup auth path was already bounded via credentialTimeoutMs;
this extends the same bound to the runtime path:

- TryGetCredentials takes credentialTimeoutMs (default
  DefaultCredentialTimeoutMs) and plumbs it to TryCallGitCredential.
- RejectCredentials, which reloads the credential on the 401-retry leg,
  takes and plumbs the same timeout (this leg is the actual stale-token
  hang path and was otherwise still unbounded).
- HttpRequestor exposes a protected virtual CredentialTimeoutMs and
  passes it to both TryGetCredentials and RejectCredentials.

The runtime bound uses the generous BackgroundCredentialTimeoutMs (120s)
rather than the 30s default: the mount's requestor is shared by the
background maintenance prefetch, interactive on-demand hydration, and the
user-initiated prefetch/clone verbs, where a human may legitimately take
longer than 30s to answer a GCM cold-start / MFA / smartcard prompt. 120s
still bounds the hang while being long enough not to cut off a prompt the
user is actively answering.

On timeout the fetch fails, backoff engages, the download gives up, and
the lock is released instead of hanging forever.

Adds unit tests covering the timeout and explicit-timeout success paths.

Fixes AB#63011829

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
@tyrielv tyrielv force-pushed the tyrielv/prefetch-auth-timeout branch from 12eb591 to 2732ff8 Compare July 10, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant