Skip to content

Commit eb66631

Browse files
authored
Merge pull request #1991 from microsoft/milestones/m275.2
Release M275.2
2 parents 342205c + b65af3a commit eb66631

19 files changed

Lines changed: 771 additions & 77 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Official-release NuGet configuration. Pipeline-only.
4+
5+
Used by the official release pipeline ONLY, supplied at build time via
6+
-ConfigFile to a one-off `nuget install Microsoft.Build.Vcpkg` command
7+
that fetches the Terrapin retrieval tool. The root nuget.config (the
8+
one external contributors and the public GitHub Actions workflow use)
9+
does NOT reference this internal feed, so building VFS for Git from a
10+
fresh clone requires no internal authentication.
11+
12+
Why this file exists:
13+
Microsoft.Build.Vcpkg is an internal-only NuGet package that ships
14+
the proprietary TerrapinRetrievalTool.exe. The release pipeline
15+
downloads the package out-of-band (i.e. not via msbuild SDK
16+
resolution, which would force the internal feed into the root
17+
nuget.config and expose it to every consumer) and passes the path to
18+
the extracted retrieval tool via -p:TerrapinRetrievalToolPath. vcpkg
19+
then routes its asset downloads through the Terrapin cache, because
20+
the release pipeline's build agents have x-block-origin enforced at
21+
the network layer and cannot fetch from public origins
22+
(sourceforge.net, github.com release artifacts, etc.).
23+
-->
24+
<configuration>
25+
<packageSources>
26+
<clear />
27+
<add key="GitClient" value="https://pkgs.dev.azure.com/mseng/1ES/_packaging/GitClient/nuget/v3/index.json" />
28+
</packageSources>
29+
</configuration>

.azure-pipelines/release.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ extends:
111111
inputs:
112112
versionSpec: '6.x'
113113

114+
- task: NuGetAuthenticate@1
115+
displayName: 'Authenticate to internal NuGet feed (for Microsoft.Build.Vcpkg)'
116+
114117
- task: PowerShell@2
115118
displayName: 'Install VS C++ workload (NativeAOT prerequisite)'
116119
inputs:
@@ -121,6 +124,37 @@ extends:
121124
inputs:
122125
filePath: $(Build.SourcesDirectory)\.azure-pipelines\scripts\enable-projfs.ps1
123126

127+
# Download the Microsoft.Build.Vcpkg NuGet package out-of-band so we
128+
# can hand the build a path to TerrapinRetrievalTool.exe via
129+
# -p:TerrapinRetrievalToolPath. The package is pulled from an
130+
# internal NuGet feed (see .azure-pipelines/official-release-nuget.config).
131+
# Downloading it this way -- rather than as an msbuild Sdk import --
132+
# keeps the internal feed out of the root nuget.config that
133+
# external contributors and the public GitHub Actions workflow see.
134+
- task: NuGetCommand@2
135+
displayName: 'Download Microsoft.Build.Vcpkg package (Terrapin retrieval tool)'
136+
inputs:
137+
command: custom
138+
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'
139+
140+
# Restore vcpkg native dependencies through the Terrapin asset
141+
# cache (the release pipeline's build agents have x-block-origin
142+
# enforced and cannot download from the public internet). Runs the
143+
# _RestoreVcpkgDependencies MSBuild target with
144+
# UseTerrapinAssetCache=true and TerrapinRetrievalToolPath pointing
145+
# at the binary extracted by the previous step. vcpkg downloads
146+
# then route through https://vcpkg.storage.devpackages.microsoft.io.
147+
# Build.bat's own vcpkg install step then skips because the libs
148+
# are already present.
149+
- script: |
150+
dotnet build "$(Build.SourcesDirectory)\GVFS\GVFS.Common\GVFS.Common.csproj" ^
151+
/t:_RestoreVcpkgDependencies ^
152+
-c $(BuildConfiguration) ^
153+
-p:UseTerrapinAssetCache=true ^
154+
-p:TerrapinRetrievalToolPath=$(Agent.TempDirectory)\nuget-internal\Microsoft.Build.Vcpkg\trt\TerrapinRetrievalTool.exe ^
155+
-v:detailed
156+
displayName: 'Restore vcpkg native libraries (Terrapin cache)'
157+
124158
- script: |
125159
$(Build.SourcesDirectory)\scripts\Build.bat ^
126160
$(BuildConfiguration) ^

Directory.Build.props

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@
1818
but our .NET projects do.
1919
-->
2020
<RestorePackagesConfig>true</RestorePackagesConfig>
21+
22+
<!--
23+
Set to true when building locally to allow Terrapin to upload missing
24+
vcpkg assets to the shared cache. CI agents must keep this false so
25+
vcpkg uses x-block-origin (fail fast instead of attempting direct
26+
downloads from the public internet, which is blocked at the agent
27+
network layer).
28+
-->
29+
<IsLocalBuild Condition="'$(IsLocalBuild)' == ''">false</IsLocalBuild>
30+
31+
<!--
32+
Set to true on the official release pipeline to route vcpkg asset
33+
downloads through the internal Terrapin cache via the
34+
Microsoft.Build.Vcpkg SDK (an internal-only package). Defaults to
35+
false so external contributors and the public GitHub Actions
36+
PR-validation workflow continue to use the standard vcpkg flow
37+
with direct downloads from the public internet.
38+
-->
39+
<UseTerrapinAssetCache Condition="'$(UseTerrapinAssetCache)' == ''">false</UseTerrapinAssetCache>
2140
</PropertyGroup>
2241

2342
<!-- Managed project properties -->

Directory.Build.targets

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@
3232
<_VcpkgManifestFile>$(RepoSrcPath)vcpkg.json</_VcpkgManifestFile>
3333
<_VcpkgConfigFile>$(RepoSrcPath)vcpkg-configuration.json</_VcpkgConfigFile>
3434
<_VcpkgStampFile>$(RepoOutPath)vcpkg_installed\.msbuildstamp</_VcpkgStampFile>
35+
36+
<!--
37+
Terrapin asset caching for vcpkg downloads (only when explicitly
38+
enabled by the release pipeline). -a $(IsLocalBuild) controls whether
39+
the retrieval script may upload missing assets back to the Terrapin
40+
cache: true on dev boxes (write permission available), false on CI
41+
agents (read-only; x-block-origin causes immediate failure rather
42+
than attempting public-internet downloads).
43+
44+
$(TerrapinRetrievalToolPath) is supplied by the release pipeline
45+
after it downloads the internal Microsoft.Build.Vcpkg NuGet package
46+
that ships the retrieval tool. The repo intentionally does not
47+
reference that internal package directly; the pipeline is the only
48+
bridge between this repo and the internal feed.
49+
-->
50+
<_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>
3551
</PropertyGroup>
3652

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

7288
<MakeDir Directories="$(RepoOutPath)vcpkg_installed\" />
73-
<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)"
89+
90+
<!--
91+
Guard: if asset caching was requested but the tool path was not
92+
supplied, the asset-source string would be malformed and vcpkg
93+
would fail with a confusing error. Surface the misconfiguration up
94+
front.
95+
-->
96+
<Error Condition="'$(UseTerrapinAssetCache)' == 'true' and ('$(TerrapinRetrievalToolPath)' == '' or !Exists('$(TerrapinRetrievalToolPath)'))"
97+
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;." />
98+
99+
<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)"
74100
StandardOutputImportance="High" StandardErrorImportance="High" />
75-
<Exec Command="&quot;$(_VcpkgExe)&quot; install --x-wait-for-lock --triplet x64-windows-dynamic --x-install-root=&quot;$(RepoOutPath)vcpkg_installed\dynamic&quot; $(_VcpkgManifestArg)"
101+
<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)"
76102
StandardOutputImportance="High" StandardErrorImportance="High" />
77103

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

81109
<Touch Files="$(_VcpkgStampFile)" AlwaysCreate="true" />
82110
</Target>

GVFS/GVFS.Common/GVFSLock.cs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ public bool TryAcquireLockForExternalRequestor(
4141

4242
existingExternalHolder = null;
4343

44+
// Capture the requestor's process start time so we can later distinguish the
45+
// genuine holder from an unrelated process that happens to be reusing the same
46+
// PID after the holder exits. If we cannot read the start time (e.g. permission
47+
// failure on OpenProcess for a different-integrity caller) we still accept the
48+
// lock and fall back to the legacy PID-only orphan check; record the fallback in
49+
// telemetry so we can spot if it becomes common.
50+
long? requestorStartTime = GVFSPlatform.Instance.TryGetActiveProcessStartTime(requestor.PID, out long startTime)
51+
? startTime
52+
: (long?)null;
53+
if (requestorStartTime == null)
54+
{
55+
metadata.Add("StartTimeUnavailable", true);
56+
}
57+
4458
try
4559
{
4660
lock (this.acquisitionLock)
@@ -65,7 +79,7 @@ public bool TryAcquireLockForExternalRequestor(
6579
metadata.Add("Result", "Accepted");
6680
eventLevel = EventLevel.Informational;
6781

68-
this.currentLockHolder.AcquireForExternalRequestor(requestor);
82+
this.currentLockHolder.AcquireForExternalRequestor(requestor, requestorStartTime);
6983
this.Stats = new ActiveGitCommandStats();
7084

7185
return true;
@@ -190,12 +204,14 @@ private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages
190204
}
191205

192206
bool externalHolderTerminatedWithoutReleasingLock;
207+
string terminationReason;
193208
existingExternalHolder = this.currentLockHolder.GetExternalHolder(
194-
out externalHolderTerminatedWithoutReleasingLock);
209+
out externalHolderTerminatedWithoutReleasingLock,
210+
out terminationReason);
195211

196212
if (externalHolderTerminatedWithoutReleasingLock)
197213
{
198-
this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID);
214+
this.ReleaseLockForTerminatedProcess(existingExternalHolder.PID, terminationReason);
199215
this.tracer.SetGitCommandSessionId(string.Empty);
200216
existingExternalHolder = null;
201217
}
@@ -204,11 +220,11 @@ private bool IsLockAvailable(bool checkExternalHolderOnly, out NamedPipeMessages
204220
}
205221
}
206222

207-
private bool ReleaseExternalLock(int pid, string eventName)
223+
private bool ReleaseExternalLock(int pid, string eventName, EventMetadata extraMetadata = null)
208224
{
209225
lock (this.acquisitionLock)
210226
{
211-
EventMetadata metadata = new EventMetadata();
227+
EventMetadata metadata = extraMetadata ?? new EventMetadata();
212228

213229
try
214230
{
@@ -251,9 +267,11 @@ private bool ReleaseExternalLock(int pid, string eventName)
251267
}
252268
}
253269

254-
private void ReleaseLockForTerminatedProcess(int pid)
270+
private void ReleaseLockForTerminatedProcess(int pid, string terminationReason)
255271
{
256-
this.ReleaseExternalLock(pid, "ExternalLockHolderExited");
272+
EventMetadata metadata = new EventMetadata();
273+
metadata.Add("ExternalHolderTerminationReason", terminationReason ?? "Unknown");
274+
this.ReleaseExternalLock(pid, "ExternalLockHolderExited", metadata);
257275
}
258276

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

387406
public bool IsFree
388407
{
@@ -404,7 +423,7 @@ public void AcquireForGVFS()
404423
this.IsGVFS = true;
405424
}
406425

407-
public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder)
426+
public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockHolder, long? startTime)
408427
{
409428
if (this.IsGVFS ||
410429
this.externalLockHolder != null)
@@ -413,27 +432,59 @@ public void AcquireForExternalRequestor(NamedPipeMessages.LockData externalLockH
413432
}
414433

415434
this.externalLockHolder = externalLockHolder;
435+
this.externalLockHolderStartTime = startTime;
416436
}
417437

418438
public void Release()
419439
{
420440
this.IsGVFS = false;
421441
this.externalLockHolder = null;
442+
this.externalLockHolderStartTime = null;
422443
}
423444

424445
public NamedPipeMessages.LockData GetExternalHolder()
425446
{
426447
return this.externalLockHolder;
427448
}
428449

429-
public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock)
450+
public NamedPipeMessages.LockData GetExternalHolder(out bool externalHolderTerminatedWithoutReleasingLock, out string terminationReason)
430451
{
431452
externalHolderTerminatedWithoutReleasingLock = false;
453+
terminationReason = null;
432454

433455
if (this.externalLockHolder != null)
434456
{
435457
int pid = this.externalLockHolder.PID;
436-
externalHolderTerminatedWithoutReleasingLock = !GVFSPlatform.Instance.IsProcessActive(pid);
458+
459+
if (this.externalLockHolderStartTime is long capturedStartTime)
460+
{
461+
// Identity check: confirm the same process still owns this PID by comparing
462+
// the OS-supplied process start time we captured at acquisition with the
463+
// current one. A mismatch means the original holder exited and Windows
464+
// recycled the PID to a different process (the bug this code fixes).
465+
if (!GVFSPlatform.Instance.TryGetActiveProcessStartTime(pid, out long currentStartTime))
466+
{
467+
externalHolderTerminatedWithoutReleasingLock = true;
468+
terminationReason = "ProcessNotActive";
469+
}
470+
else if (currentStartTime != capturedStartTime)
471+
{
472+
externalHolderTerminatedWithoutReleasingLock = true;
473+
terminationReason = "PidRecycled";
474+
}
475+
}
476+
else
477+
{
478+
// Fallback for the rare case where we could not capture a start time at
479+
// acquisition time (e.g. cross-integrity OpenProcess denial). Use the
480+
// legacy PID-only liveness check, which is vulnerable to PID recycling
481+
// but matches pre-fix behavior.
482+
if (!GVFSPlatform.Instance.IsProcessActive(pid))
483+
{
484+
externalHolderTerminatedWithoutReleasingLock = true;
485+
terminationReason = "ProcessNotActive";
486+
}
487+
}
437488
}
438489

439490
return this.externalLockHolder;

GVFS/GVFS.Common/GVFSPlatform.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ public static void Register(GVFSPlatform platform)
6666
public abstract void PrepareProcessToRunInBackground();
6767

6868
public abstract bool IsProcessActive(int processId);
69+
70+
/// <summary>
71+
/// Returns true and writes an opaque, OS-supplied process-identity token (e.g. process
72+
/// creation time on Windows) when the process with the given PID is currently active.
73+
/// The token has no meaning beyond identity comparison: two calls for the same underlying
74+
/// process yield equal tokens, and a call after the OS has recycled the PID to a different
75+
/// process yields a different token. Returns false (and startTime = 0) if the process
76+
/// is no longer running, or if it could not be identified for any reason (e.g. permission
77+
/// failure). Callers must treat a false return as "no identity information available" and
78+
/// fall back to <see cref="IsProcessActive(int)"/> if they still need a liveness check.
79+
/// </summary>
80+
public abstract bool TryGetActiveProcessStartTime(int processId, out long startTime);
81+
6982
public abstract void IsServiceInstalledAndRunning(string name, out bool installed, out bool running);
7083
public abstract string GetNamedPipeName(string enlistmentRoot);
7184
public abstract string GetGVFSServiceNamedPipeName(string serviceName);

0 commit comments

Comments
 (0)