Skip to content

Release M279.3 - #2092

Open
tyrielv wants to merge 12 commits into
releases/shippedfrom
milestones/M279.3
Open

Release M279.3#2092
tyrielv wants to merge 12 commits into
releases/shippedfrom
milestones/M279.3

Conversation

@tyrielv

@tyrielv tyrielv commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Changes:

tyrielv and others added 12 commits August 7, 2026 15:03
When a user process reads a virtualized placeholder whose stored
content-id is corrupt - specifically 40 NUL bytes instead of a hex SHA -
GVFS builds a loose-object path from it and Path.Combine throws
System.ArgumentException ("Illegal characters in path").

ArgumentException is not in RetryWrapper.IsHandlableException, so it
bypasses both the retry logic and the download fallback in
GVFSGitObjects.TryCopyBlobContentStream and propagates to the
virtualizer's outer catch, which returns FileNotAvailable to ProjFS. The
placeholder can never hydrate, so the failing read repeats forever - a
retry storm. This is the #1 blob-hydration failure cause on the LKG field
build 1.0.26014.1 (38 machines; ~61 machines / ~6.2K events across 30d;
one machine emitted ~2.49M error events).

This is a corrupt content-id, NOT GVFSConstants.AllZeroSha: AllZeroSha is
40 ASCII '0' characters, which yields directory "00" and does not throw.

Reject a malformed SHA before it is turned into a path:

- GitRepo.GetLooseBlobState returns LooseBlobState.Invalid (a clean,
  non-retryable miss) for a SHA that is not 40 hex characters, so
  Path.Combine can never throw here again.
- GitRepo.LooseObjectExists guards the same Path.Combine.
- GVFSGitObjects.TryCopyBlobContentStream short-circuits a malformed SHA
  before the retry loop, so a bogus SHA never triggers a doomed 404
  download or a retry storm.
- SHA1Util.IsValidShaFormat is now null-safe; SHA1Util.ToLoggableShaString
  renders the bad value with non-hex characters escaped so telemetry stays
  greppable and free of control characters.
- WindowsFileSystemVirtualizer routes the request's logged sha through
  ToLoggableShaString, so a malformed content-id can no longer enter
  telemetry with raw NUL/control bytes at the terminal hydration-failure
  error either (a no-op for a valid hex SHA).

All three guard sites emit the same greppable Warning event
(*_MalformedBlobSha) at Warning level with no unhandled exception. Per an
existing decision this case stays telemetry category "Unexpected"; no new
BlobHydrationFailureCategory is added.

Stacked on #2071 (tyrielv/split-hydration-enum-telemetry): this branch is
rebased onto it, so #2071's out BlobHydrationFailureCategory parameter is
honored - the malformed-SHA short-circuit sets failureCategory =
Unexpected, so the virtualizer's terminal telemetry tags the case exactly
as before (it no longer reaches the outer catch because it no longer
throws). This PR must NOT merge before #2071; after #2071 lands, rebase
onto master.

Unit tests assert that a 40-NUL-byte SHA, a 40-char SHA with an embedded
path-illegal character, and other malformed SHAs return false from both
GitRepo.TryCopyBlobContentStream and GVFSGitObjects.TryCopyBlobContentStream
with no ArgumentException (Assert.DoesNotThrow), that no download/retry is
attempted, and that the out category is Unexpected.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
….vfs.0.8

Update default Microsoft Git version to v2.55.0.vfs.0.8
When an on-demand loose-blob download fails, GVFS emits a terminal
telemetry error with a BlobHydrationFailureCategory. The DownloadFailed
category is the transient or unclassified bucket. It collapses genuine
auth failures (401, 400, 302) and transient failures (timeout 408, 5xx,
pool-exhaustion 503) into one value.

The HTTP status of the failing download is known in the code, but it
only reaches the on-box log, not shipped telemetry. So telemetry cannot
tell a real auth failure apart from a transient one.

Carry the HTTP status of the last download attempt to the terminal
failure event through an internal DownloadAttemptResult type. Add
HttpStatusCode and HttpStatusName to the event metadata only when the
failure is attributable to the download itself (DownloadFailed or
ObjectNotOnServer), so an earlier attempt's status cannot attach to a
later local-IO or copy failure. The public TryDownloadAndSaveObject
return type does not change.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…Repo

git_config_get_string returns a borrowed pointer whose lifetime is tied to
the config object, so libgit2 only permits it on a snapshot (read-only)
config. LibGit2Repo.GetConfigString called it on the live config returned by
git_repository_config, which fails with "get_string called on a live config
object". The read then threw LibGit2Exception, and callers silently fell back
to their default value instead of honoring the configured setting.

Take a git_config_snapshot of the live config and read the string from the
snapshot, freeing the snapshot afterward.

Also stop marshalling the result as an out string. git_config_get_string
returns a borrowed const char* owned by the config; the interop marshaller
would free that pointer with CoTaskMemFree, a mismatched-allocator free of
memory libgit2 still owns, corrupting the heap. Retrieve the value as an
IntPtr and copy it with Marshal.PtrToStringUTF8, which never frees the
borrowed pointer. This matches the manual marshalling already used by
GitConfigEntry.

Not-found still returns null so the documented default applies without a
spurious error. git_config_get_bool is unaffected (it parses the value rather
than returning a borrowed pointer), so GetConfigBool is left unchanged.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Add LibGit2ConfigTests, a functional test that exercises the real libgit2
(git2.dll) config-read path in LibGit2Repo against a plain on-disk git
repository. It creates a temp repo, sets a string and a bool config value,
then reads them back through LibGit2Repo and asserts a missing key returns
null.

No unit test can cover this: the unit tests mock the native layer, so the
"get_string called on a live config object" failure only manifests through
the real P/Invoke. This test fails before the snapshot/marshalling fix and
passes after, guarding the regression.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
The malformed-SHA guard in TryCopyBlobContentStream only covers the blob
hydration path. Two other callers reach the object download directly, so a
corrupt (NUL-byte) placeholder SHA from them still reached the network:

- the git.exe read-object hook (RequestSource.NamedPipeMessage, via
  InProcessMount), and
- the gitattributes GVFSVerb (RequestSource.GVFSVerb).

On .NET Framework the local Path.Combine threw ArgumentException on such a
value, so the download was never reached. On modern .NET (which 2.0 runs)
Path.Combine no longer validates path characters, so the malformed SHA
silently misses the local object store and is sent to the cache server. The
Application Gateway rejects the malformed URL with HTTP 400, and GVFS then
treats the 400 as an auth failure and erases a valid credential, producing a
credential-prompt storm (ICM 850075166).

Reject a malformed object SHA at the download chokepoint
(GVFSGitObjects.TryDownloadAndSaveObject, next to the existing AllZeroSha
guard) for every request source, before any request is built, and emit the
same greppable *_MalformedBlobSha Warning as the other guards. Also correct
the GetLooseBlobState comment: the ArgumentException it described is
.NET-Framework-only, so validating (not relying on the throw) is what makes
the guard correct on modern .NET.

Unit test asserts a malformed SHA returns Error from TryDownloadAndSaveObject
across FileStreamCallback / NamedPipeMessage / GVFSVerb, never reaches the
network (download call count stays 0), and is logged.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…-getstring

Read git config strings via a config snapshot in LibGit2Repo
Fail blob hydration cleanly on a malformed (NUL-byte) placeholder SHA
Record HTTP status code on blob-hydration failure telemetry
The malformed-SHA guard in TryDownloadAndSaveObject returned the bare
DownloadAndSaveObjectResult enum. The method now returns DownloadAttemptResult
(the enum plus an HTTP status code), so the build failed with CS0029. Wrap the
error in a DownloadAttemptResult with a null status code, matching the
AllZeroSha guard just below it.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…esult

Fix build break: return DownloadAttemptResult from the malformed-SHA guard
@tyrielv
tyrielv enabled auto-merge August 14, 2026 22:41
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.

2 participants