Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .azure-pipelines/official-release-nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Official-release NuGet configuration. Pipeline-only.

Used by the official release pipeline ONLY, supplied at build time via
-ConfigFile to a one-off `nuget install Microsoft.Build.Vcpkg` command
that fetches the Terrapin retrieval tool. The root nuget.config (the
one external contributors and the public GitHub Actions workflow use)
does NOT reference this internal feed, so building VFS for Git from a
fresh clone requires no internal authentication.

Why this file exists:
Microsoft.Build.Vcpkg is an internal-only NuGet package that ships
the proprietary TerrapinRetrievalTool.exe. The release pipeline
downloads the package out-of-band (i.e. not via msbuild SDK
resolution, which would force the internal feed into the root
nuget.config and expose it to every consumer) and passes the path to
the extracted retrieval tool via -p:TerrapinRetrievalToolPath. vcpkg
then routes its asset downloads through the Terrapin cache, because
the release pipeline's build agents have x-block-origin enforced at
the network layer and cannot fetch from public origins
(sourceforge.net, github.com release artifacts, etc.).
-->
<configuration>
<packageSources>
<clear />
<add key="GitClient" value="https://pkgs.dev.azure.com/mseng/1ES/_packaging/GitClient/nuget/v3/index.json" />
</packageSources>
</configuration>
34 changes: 34 additions & 0 deletions .azure-pipelines/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ extends:
inputs:
versionSpec: '6.x'

- task: NuGetAuthenticate@1
displayName: 'Authenticate to internal NuGet feed (for Microsoft.Build.Vcpkg)'

- task: PowerShell@2
displayName: 'Install VS C++ workload (NativeAOT prerequisite)'
inputs:
Expand All @@ -121,6 +124,37 @@ extends:
inputs:
filePath: $(Build.SourcesDirectory)\.azure-pipelines\scripts\enable-projfs.ps1

# Download the Microsoft.Build.Vcpkg NuGet package out-of-band so we
# can hand the build a path to TerrapinRetrievalTool.exe via
# -p:TerrapinRetrievalToolPath. The package is pulled from an
# internal NuGet feed (see .azure-pipelines/official-release-nuget.config).
# Downloading it this way -- rather than as an msbuild Sdk import --
# keeps the internal feed out of the root nuget.config that
# external contributors and the public GitHub Actions workflow see.
- task: NuGetCommand@2
displayName: 'Download Microsoft.Build.Vcpkg package (Terrapin retrieval tool)'
inputs:
command: custom
arguments: 'install Microsoft.Build.Vcpkg -Version 2026.5.25.434-aa40adda53 -ConfigFile $(Build.SourcesDirectory)\.azure-pipelines\official-release-nuget.config -OutputDirectory $(Agent.TempDirectory)\nuget-internal -ExcludeVersion -DirectDownload -NonInteractive'

# Restore vcpkg native dependencies through the Terrapin asset
# cache (the release pipeline's build agents have x-block-origin
# enforced and cannot download from the public internet). Runs the
# _RestoreVcpkgDependencies MSBuild target with
# UseTerrapinAssetCache=true and TerrapinRetrievalToolPath pointing
# at the binary extracted by the previous step. vcpkg downloads
# then route through https://vcpkg.storage.devpackages.microsoft.io.
# Build.bat's own vcpkg install step then skips because the libs
# are already present.
- script: |
dotnet build "$(Build.SourcesDirectory)\GVFS\GVFS.Common\GVFS.Common.csproj" ^
/t:_RestoreVcpkgDependencies ^
-c $(BuildConfiguration) ^
-p:UseTerrapinAssetCache=true ^
-p:TerrapinRetrievalToolPath=$(Agent.TempDirectory)\nuget-internal\Microsoft.Build.Vcpkg\trt\TerrapinRetrievalTool.exe ^
-v:detailed
displayName: 'Restore vcpkg native libraries (Terrapin cache)'

- script: |
$(Build.SourcesDirectory)\scripts\Build.bat ^
$(BuildConfiguration) ^
Expand Down
19 changes: 19 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@
but our .NET projects do.
-->
<RestorePackagesConfig>true</RestorePackagesConfig>

<!--
Set to true when building locally to allow Terrapin to upload missing
vcpkg assets to the shared cache. CI agents must keep this false so
vcpkg uses x-block-origin (fail fast instead of attempting direct
downloads from the public internet, which is blocked at the agent
network layer).
-->
<IsLocalBuild Condition="'$(IsLocalBuild)' == ''">false</IsLocalBuild>

<!--
Set to true on the official release pipeline to route vcpkg asset
downloads through the internal Terrapin cache via the
Microsoft.Build.Vcpkg SDK (an internal-only package). Defaults to
false so external contributors and the public GitHub Actions
PR-validation workflow continue to use the standard vcpkg flow
with direct downloads from the public internet.
-->
<UseTerrapinAssetCache Condition="'$(UseTerrapinAssetCache)' == ''">false</UseTerrapinAssetCache>
</PropertyGroup>

<!-- Managed project properties -->
Expand Down
34 changes: 31 additions & 3 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
<_VcpkgManifestFile>$(RepoSrcPath)vcpkg.json</_VcpkgManifestFile>
<_VcpkgConfigFile>$(RepoSrcPath)vcpkg-configuration.json</_VcpkgConfigFile>
<_VcpkgStampFile>$(RepoOutPath)vcpkg_installed\.msbuildstamp</_VcpkgStampFile>

<!--
Terrapin asset caching for vcpkg downloads (only when explicitly
enabled by the release pipeline). -a $(IsLocalBuild) controls whether
the retrieval script may upload missing assets back to the Terrapin
cache: true on dev boxes (write permission available), false on CI
agents (read-only; x-block-origin causes immediate failure rather
than attempting public-internet downloads).

$(TerrapinRetrievalToolPath) is supplied by the release pipeline
after it downloads the internal Microsoft.Build.Vcpkg NuGet package
that ships the retrieval tool. The repo intentionally does not
reference that internal package directly; the pipeline is the only
bridge between this repo and the internal feed.
-->
<_VcpkgAssetSources Condition="'$(UseTerrapinAssetCache)' == 'true'">"--x-asset-sources=x-script,$(TerrapinRetrievalToolPath) -b https://vcpkg.storage.devpackages.microsoft.io/artifacts/ -a $(IsLocalBuild) -p {url} -s {sha512} -d {dst};x-block-origin"</_VcpkgAssetSources>
</PropertyGroup>

<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
Expand Down Expand Up @@ -70,13 +86,25 @@
<Message Importance="high" Text="[vcpkg] Installing native dependencies to $(RepoOutPath)vcpkg_installed\" />

<MakeDir Directories="$(RepoOutPath)vcpkg_installed\" />
<Exec Command="&quot;$(_VcpkgExe)&quot; install --x-wait-for-lock --triplet x64-windows-static-aot --x-install-root=&quot;$(RepoOutPath)vcpkg_installed\static&quot; $(_VcpkgManifestArg)"

<!--
Guard: if asset caching was requested but the tool path was not
supplied, the asset-source string would be malformed and vcpkg
would fail with a confusing error. Surface the misconfiguration up
front.
-->
<Error Condition="'$(UseTerrapinAssetCache)' == 'true' and ('$(TerrapinRetrievalToolPath)' == '' or !Exists('$(TerrapinRetrievalToolPath)'))"
Text="UseTerrapinAssetCache=true but TerrapinRetrievalToolPath is empty or does not exist. The release pipeline must pre-download the Microsoft.Build.Vcpkg NuGet package and pass -p:TerrapinRetrievalToolPath=&lt;path-to-TerrapinRetrievalTool.exe&gt;." />

<Exec Command="&quot;$(_VcpkgExe)&quot; install --x-wait-for-lock --triplet x64-windows-static-aot --x-install-root=&quot;$(RepoOutPath)vcpkg_installed\static&quot; $(_VcpkgManifestArg) $(_VcpkgAssetSources)"
StandardOutputImportance="High" StandardErrorImportance="High" />
<Exec Command="&quot;$(_VcpkgExe)&quot; install --x-wait-for-lock --triplet x64-windows-dynamic --x-install-root=&quot;$(RepoOutPath)vcpkg_installed\dynamic&quot; $(_VcpkgManifestArg)"
<Exec Command="&quot;$(_VcpkgExe)&quot; install --x-wait-for-lock --triplet x64-windows-dynamic --x-install-root=&quot;$(RepoOutPath)vcpkg_installed\dynamic&quot; $(_VcpkgManifestArg) $(_VcpkgAssetSources)"
StandardOutputImportance="High" StandardErrorImportance="High" />

<Error Condition="!Exists('$(RepoOutPath)vcpkg_installed\dynamic\x64-windows-dynamic\bin\git2.dll')"
Text="vcpkg install completed but expected output files are missing. Check vcpkg output above for errors." />
Text="vcpkg install completed but git2.dll (dynamic) is missing. Check vcpkg output above for errors." />
<Error Condition="!Exists('$(RepoOutPath)vcpkg_installed\static\x64-windows-static-aot\lib\git2.lib')"
Text="vcpkg install completed but git2.lib (static) is missing. Check vcpkg output above for errors." />

<Touch Files="$(_VcpkgStampFile)" AlwaysCreate="true" />
</Target>
Expand Down
71 changes: 61 additions & 10 deletions GVFS/GVFS.Common/GVFSLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ public bool TryAcquireLockForExternalRequestor(

existingExternalHolder = null;

// Capture the requestor's process start time so we can later distinguish the
// genuine holder from an unrelated process that happens to be reusing the same
// PID after the holder exits. If we cannot read the start time (e.g. permission
// failure on OpenProcess for a different-integrity caller) we still accept the
// lock and fall back to the legacy PID-only orphan check; record the fallback in
// telemetry so we can spot if it becomes common.
long? requestorStartTime = GVFSPlatform.Instance.TryGetActiveProcessStartTime(requestor.PID, out long startTime)
? startTime
: (long?)null;
if (requestorStartTime == null)
{
metadata.Add("StartTimeUnavailable", true);
}

try
{
lock (this.acquisitionLock)
Expand All @@ -65,7 +79,7 @@ public bool TryAcquireLockForExternalRequestor(
metadata.Add("Result", "Accepted");
eventLevel = EventLevel.Informational;

this.currentLockHolder.AcquireForExternalRequestor(requestor);
this.currentLockHolder.AcquireForExternalRequestor(requestor, requestorStartTime);
this.Stats = new ActiveGitCommandStats();

return true;
Expand Down Expand Up @@ -190,12 +204,14 @@ private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages
}

bool externalHolderTerminatedWithoutReleasingLock;
string terminationReason;
existingExternalHolder = this.currentLockHolder.GetExternalHolder(
out externalHolderTerminatedWithoutReleasingLock);
out externalHolderTerminatedWithoutReleasingLock,
out terminationReason);

if (externalHolderTerminatedWithoutReleasingLock)
{
this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID);
this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID, terminationReason);
this.tracer.SetGitCommandSessionId(string.Empty);
existingExternalHolder = null;
}
Expand All @@ -204,11 +220,11 @@ private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages
}
}

private bool ReleaseExternalLock(int pid, string eventName)
private bool ReleaseExternalLock(int pid, string eventName, EventMetadata extraMetadata = null)
{
lock (this.acquisitionLock)
{
EventMetadata metadata = new EventMetadata();
EventMetadata metadata = extraMetadata ?? new EventMetadata();

try
{
Expand Down Expand Up @@ -251,9 +267,11 @@ private bool ReleaseExternalLock(int pid, string eventName)
}
}

private void ReleaseLockForTerminatedProcess(int pid)
private void ReleaseLockForTerminatedProcess(int pid, string terminationReason)
{
this.ReleaseExternalLock(pid, "ExternalLockHolderExited");
EventMetadata metadata = new EventMetadata();
metadata.Add("ExternalHolderTerminationReason", terminationReason ?? "Unknown");
this.ReleaseExternalLock(pid, "ExternalLockHolderExited", metadata);
}

// The lock release event is a convenient place to record stats about things that happened while a git command was running,
Expand Down Expand Up @@ -383,6 +401,7 @@ public void AddStatsToTelemetry(EventMetadata metadata)
private class LockHolder
{
private NamedPipeMessages.LockData externalLockHolder;
private long? externalLockHolderStartTime;

public bool IsFree
{
Expand All @@ -404,7 +423,7 @@ public void AcquireForGVFS()
this.IsGVFS = true;
}

public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder)
public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder, long? startTime)
{
if (this.IsGVFS ||
this.externalLockHolder != null)
Expand All @@ -413,27 +432,59 @@ public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockH
}

this.externalLockHolder = externalLockHolder;
this.externalLockHolderStartTime = startTime;
}

public void Release()
{
this.IsGVFS = false;
this.externalLockHolder = null;
this.externalLockHolderStartTime = null;
}

public NamedPipeMessages.LockData GetExternalHolder()
{
return this.externalLockHolder;
}

public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock)
public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock, out string terminationReason)
{
externalHolderTerminatedWithoutReleasingLock = false;
terminationReason = null;

if (this.externalLockHolder != null)
{
int pid = this.externalLockHolder.PID;
externalHolderTerminatedWithoutReleasingLock = !GVFSPlatform.Instance.IsProcessActive(pid);

if (this.externalLockHolderStartTime is long capturedStartTime)
{
// Identity check: confirm the same process still owns this PID by comparing
// the OS-supplied process start time we captured at acquisition with the
// current one. A mismatch means the original holder exited and Windows
// recycled the PID to a different process (the bug this code fixes).
if (!GVFSPlatform.Instance.TryGetActiveProcessStartTime(pid, out long currentStartTime))
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "ProcessNotActive";
}
else if (currentStartTime != capturedStartTime)
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "PidRecycled";
}
}
else
{
// Fallback for the rare case where we could not capture a start time at
// acquisition time (e.g. cross-integrity OpenProcess denial). Use the
// legacy PID-only liveness check, which is vulnerable to PID recycling
// but matches pre-fix behavior.
if (!GVFSPlatform.Instance.IsProcessActive(pid))
{
externalHolderTerminatedWithoutReleasingLock = true;
terminationReason = "ProcessNotActive";
}
}
}

return this.externalLockHolder;
Expand Down
13 changes: 13 additions & 0 deletions GVFS/GVFS.Common/GVFSPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ public static void Register(GVFSPlatform platform)
public abstract void PrepareProcessToRunInBackground();

public abstract bool IsProcessActive(int processId);

/// <summary>
/// Returns true and writes an opaque, OS-supplied process-identity token (e.g. process
/// creation time on Windows) when the process with the given PID is currently active.
/// The token has no meaning beyond identity comparison: two calls for the same underlying
/// process yield equal tokens, and a call after the OS has recycled the PID to a different
/// process yields a different token. Returns false (and startTime = 0) if the process
/// is no longer running, or if it could not be identified for any reason (e.g. permission
/// failure). Callers must treat a false return as "no identity information available" and
/// fall back to <see cref="IsProcessActive(int)"/> if they still need a liveness check.
/// </summary>
public abstract bool TryGetActiveProcessStartTime(int processId, out long startTime);

public abstract void IsServiceInstalledAndRunning(string name, out bool installed, out bool running);
public abstract string GetNamedPipeName(string enlistmentRoot);
public abstract string GetGVFSServiceNamedPipeName(string serviceName);
Expand Down
Loading
Loading