Repository navigation
Keep authentication adaptive after indeterminate /gvfs/config probes - #2104
Conversation
61dd891 to
7ef10af
Compare
Nathan Baird (ShiningMassXAcc)
left a comment
There was a problem hiding this comment.
Reviewed the full diff plus the surrounding auth/HTTP paths. The core change is sound: IsAnonymous now defaults false, all four HttpRequestor consumers snapshot into a single sendAnonymous, and the initial probe forces anonymous so it cannot wait on the initialization it is performing. Good unit coverage of the three probe outcomes, and CI is green across the full functional + upgrade matrix on x64 and arm64.
Seven findings, ranked.
1. High — GVFS/GVFS.Common/Git/GitAuthentication.cs:48 (IsAnonymous default)
Flipping the default from true to false changes behavior for any requestor that runs before TryInitializeAndQueryGVFSConfig.
GVFSVerb.QueryGVFSConfig (GVFS/GVFS/CommandLine/GVFSVerb.cs:343) builds a ConfigHttpRequestor and calls TryQueryGVFSConfig without forceAnonymous, and it is not guaranteed that auth was initialized first:
EnsureLocalCacheIsHealthyreaches it wheneverserverGVFSConfig == nullandGitObjectsRootis missing.PrefetchVerb(PrefetchVerb.cs:355) can hit that whenResolvedCacheServeris set butServerGVFSConfigis null, so theTryAuthenticateAndQueryGVFSConfigblock is skipped entirely.DehydrateVerb.cs:110(the maintenance-job path) callsInitializeLocalCacheAndObjectsPathswith noTryAuthenticateahead of it.
Previously that request went out anonymously. Now SendRequest takes the credential path, and TryGetCredentials blocks on initializationComplete for InitializationWaitTimeoutMs (= BackgroundCredentialTimeoutMs, 120_000) before failing with "Timed out waiting for authentication to initialize".
Could you confirm those paths always initialize first? If not, either pass forceAnonymous: true there, or make TryGetCredentials fail fast when initialization was never started (as opposed to in flight). A silent two-minute stall is a worse failure mode than the one being fixed here.
2. Medium — GVFS.UnitTests/Git/GitAuthenticationTests.cs, InitialConfigProbeIsSentWithoutCredentials
This test re-implements the thing it is testing:
// Mirror HttpRequestor.SendRequest's credential gate exactly.
bool sendAnonymous = forceAnonymous || dut.IsAnonymous;That is a hand copy of the gate in HttpRequestor.SendRequest, so the test keeps passing if that gate ever changes. The HttpRequestor.cs edits are the riskiest part of this PR and currently have no direct coverage. Could we get one test that drives the real SendRequest and asserts the Authorization header is absent under forceAnonymous and present otherwise?
3. Medium — GitAuthentication.cs, indeterminate branch (~307-326)
Roughly twenty lines of comment for three lines of code, and most of it narrates the investigation rather than the code. The Azure DevOps cache-server 400 chain, "a 400 is not retryable", "initialization is already latched - so re-initialization throws" is already in the PR description, which is where it belongs.
Suggest trimming to the two or three lines a future reader actually needs — the probe outcome is unknown, so assume auth is required; a server that allows anonymous access will ignore the header — and leaving the incident analysis in the PR and linked work item.
4. Medium — duplicated rationale across four sites
The "forceAnonymous is required, not incidental / this probe DETERMINES whether the server allows anonymous access / self-deadlock" explanation appears nearly verbatim in GitAuthentication.cs, the HttpRequestor.SendRequest param doc, IGVFSConfigRequestor.cs, and the test's XML doc. Keep one canonical version (the interface doc reads like the right home) and <see cref=""/> it from the other three. Four copies will drift.
5. Low — GitAuthentication.cs, this.IsAnonymous = false; in the indeterminate branch
The comment concedes this is a no-op: "the field already defaults to false and no earlier path in this method can set it true without returning". Either drop the assignment or keep it and drop the four-line justification — as written the justification costs more than the statement it defends.
6. Low — no assertion on the new telemetry keyword
The PR specifically calls out that the plain RelatedWarning(string, params object[]) overload traces with Keywords.None and gets filtered by TelemetryDaemonEventListener. Worth a MockTracer assertion that the indeterminate warning carries Keywords.Telemetry, otherwise the next refactor silently loses the signal this PR is adding.
7. Low — GitAuthentication.cs:48 <remarks>
"...so a default of true makes every request unauthenticated when the probe does not run or does not complete" describes the old default. Reword to describe current behavior: while this is true, SendRequest omits the Authorization header and never calls TryGetCredentials.
No stray .github AI artifacts in the diff, and no coverage gate exists in this repo (GitHub Actions only), so item 2 is the closest thing to a differential-coverage gap.
7ef10af to
17b18ab
Compare
|
Note This reply was machine-drafted and approved by the PR author before posting. Thanks, Nathan. I traced the pre-initialization callers and kept their anonymous behavior; mount now marks auth as scheduled before queuing the background initializer. I added tests that exercise real |
8970356 to
a0d39cc
Compare
Nathan Baird (ShiningMassXAcc)
left a comment
There was a problem hiding this comment.
Review pass over this PR. 8 comment(s).
These findings concern code the diff does not touch directly, so GitHub cannot anchor them inline:
GVFS/GVFS.Common/Git/GitAuthentication.cs (around line 242) - HIGH
This change moves an unbounded credential call onto a path that previously could not reach it.
Before: an indeterminate /gvfs/config probe left IsAnonymous true, so a virtualization callback never invoked the credential helper. After: the indeterminate branch (349-368) leaves IsAnonymous false and calls MarkInitialized(), so the next callback reaches TryCallGitCredential(tracer, out errorMessage) — the two-argument overload, which defaults timeoutMs to -1, i.e. Timeout.Infinite.
That call happens while holding gitAuthLock. If the credential helper blocks — GCM waiting on an interactive prompt that has nowhere to appear, a hung broker — the callback never returns, and every thread serializing on gitAuthLock follows it down. An indeterminate probe is exactly the condition under which a flaky network is likely, which is also when the helper is most likely to block.
I would not defer this to #2082: DefaultCredentialTimeoutMs is already declared at line 16 of this file and TryInitializeAndQueryGVFSConfigCore already threads a bounded timeout at line 373. Passing a bounded timeout here looks like a small change relative to the exposure.
GVFS/GVFS.Common/Git/GitAuthentication.cs (around line 368) - MEDIUM
This is a deliberate back-compat break — a genuinely anonymous-capable server whose config probe is indeterminate goes from "works anonymously" to "fails" — and there is no way to opt out of it short of downgrading.
AGENTS.md asks for new behavior gates to be gvfs.-prefixed git config keys declared in GVFSConstants.cs under GitConfig and read via GetConfigBoolOrDefault. Something like gvfs.require-auth-on-indeterminate-config, defaulting to the new (safe) behavior, would give a stuck user a documented escape rather than a rollback. gvfs.background-cache-auth is the pattern.
Separately, the error text ("Unable to query /gvfs/config") describes the probe but not the consequence — worth saying that GVFS is now assuming authentication is required, so the user knows what changed.
GVFS/GVFS/CommandLine/PrefetchVerb.cs (around line 324) - MEDIUM
The new invariant here — sendAnonymous = forceAnonymous || IsAnonymous || !IsInitializationStarted — depends on every requestor path having called MarkInitializationStarted(), but the only product call site is InProcessMount.cs:162.
InitializeServerConnection skips TryAuthenticateAndQueryGVFSConfig entirely when ResolvedCacheServer != null, which is the clone follow-on path (CloneVerb.cs:324), and GVFSVerb.Execute<TVerb> only forwards WorkingDirectoryRoot, so the sub-verb constructs a fresh GitAuthentication with initializationStarted == 0. On that path every object download is permanently anonymous, and a 401 reaches HttpRequestor.cs:262 with shouldRetry = false.
This is not a regression from your change, but it is the one remaining silently-anonymous path against a cache server, and nothing covers it. Worth either closing here or filing it alongside, so the invariant this PR introduces is actually true everywhere.
a0d39cc to
18f328c
Compare
|
Note This reply was machine-drafted and human-approved. Thanks for the detailed pass. I pushed
I also changed the fallback design. An indeterminate probe now leaves authentication in an unknown state. Later requests try anonymously first. A 401, redirect, or the cache server's authentication-required 400 promotes the state to authenticated and makes the retry use credentials. A known-anonymous state can also promote later. This removes the compatibility break without a config flag. I left credential timeout work to #2082 after verifying that it directly bounds this same I also traced the clone follow-on path. The full unit suite passes with 1,081 tests and 11 expected native-hook ignores. Release builds of GVFS and GVFS.Mount also pass. |
Nathan Baird (ShiningMassXAcc)
left a comment
There was a problem hiding this comment.
Re-review of the force-pushed 18f328c8. The tri-state AuthenticationState is a better design than the fail-closed version it replaces, and all five of my inline asks are genuinely addressed. The back-compat concern I raised is now moot — Unknown stays anonymous-first, so there's no break to gate.
Six observations below. The first one I believe makes the PR's central mechanism inert, so it's worth resolving before this lands. CI is green across every leg.
|
[KeithBot]: Validated: ✅ Full unit suite, source + merge base · ✅ Auth/HTTP fixtures, source + base · ✅ GVFS.Mount build at source · ✅ Loopback auth wire matrix at source · ❌ Judge loopback harness, source vs base · ⏭️ Functional tests / real mount Also confirmed (raised by others, reproduced by us): T4276332933 (ShiningMassXAcc) · T4276332944 (ShiningMassXAcc) 🤖 KeithBot · claude-opus-5.5, gpt-6.1-sol, gemini-4-argon · reply on a thread to dispute |
Mount probes /gvfs/config without credentials to learn whether the server requires authentication. A timeout, server error, or socket failure does not answer that question. Treating this result as anonymous caused later cache requests to omit credentials forever. Treating it as authenticated can block requests before they reach a server that accepts anonymous access. Keep authentication in one of three states: unknown, anonymous, or authenticated. After an indeterminate probe, send the next runtime request without credentials. Record anonymous access after a successful response. Promote the state to authenticated after a 401, redirect, or the cache server's authentication-required 400 response, then retry with credentials. A known-anonymous state can also promote if server behavior changes. Keep the initial config probe independent from the state that it determines. Make mount mark initialization before it queues background authentication, so callbacks wait for the initial result. Always signal initialization completion, including after exceptions. Capture probe failures in telemetry and user-visible errors. Document the dual-success credential contract. Add request-level tests for unknown, anonymous, and authenticated transitions, plus initialization failure paths. Assisted-by: Claude Opus 5 Assisted-by: GPT-5.6 Sol Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Make an authentication challenge promote requests that run before explicit initialization. Report the actual anonymous request mode in heartbeat telemetry and trace runtime authentication transitions. Preserve authentication challenge response details. Keep expected anonymous challenges out of global circuit-breaker accounting while retaining their credentialed retry. Add tests for pre-initialization promotion, transition tracing, response details, heartbeat telemetry, and circuit-breaker behavior. Assisted-by: GPT-5.6 Sol Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
18f328c to
a33ca20
Compare
Carry the retry failure-accounting decision through RetryWrapper invocation results and object-download outcomes. Blob hydration can then retry an expected anonymous authentication challenge without incrementing the global circuit breaker in its outer retry loop. Add tests for RetryWrapper propagation and the blob-hydration boundary. Assisted-by: GPT-5.6 Sol Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Problem and Context
The mount process probes
/gvfs/configwithout credentials to determine whether the repository requires authentication.A timeout, server error, or socket failure does not answer that question. The prior behavior kept anonymous mode after this failure. Later cache requests omitted the
Authorizationheader and never retrieved credentials. The cache server rejected every request, so GVFS could not hydrate files or retrieve blob sizes until remount.The opposite fallback is also incomplete. If GVFS assumes authentication is required, credential retrieval can fail before a request reaches a server that accepts anonymous access.
Changes
/gvfs/configprobe to remain anonymous, independent of the state that it determines.Known gaps and deliberate decisions