@@ -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 ;
0 commit comments