Skip to content

Commit 287bff1

Browse files
[release/9.0-staging] Fix PipeStream leak on Windows when pipe is disposed with a pending operation (#116188)
* Fix PipeStream cleanup on Windows when pipe is disposed with a pending operation The same pattern occurs in a couple of other places, as well. * Make dispose cancellation unconditional --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 74a39ca commit 287bff1

File tree

5 files changed

+33
-9
lines changed

5 files changed

+33
-9
lines changed

src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,7 @@ internal override void DisposeCore(bool disposing)
139139

140140
if (disposing)
141141
{
142-
if (State != PipeState.Closed)
143-
{
144-
_internalTokenSource.Cancel();
145-
}
142+
_internalTokenSource.Cancel();
146143
}
147144
}
148145

src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Windows.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ internal override void TryToReuse(PipeValueTaskSource source)
6262
{
6363
if (Interlocked.CompareExchange(ref _reusableConnectionValueTaskSource, connectionSource, null) is not null)
6464
{
65-
source._preallocatedOverlapped.Dispose();
65+
source.Dispose();
66+
}
67+
else if (State == PipeState.Closed)
68+
{
69+
Interlocked.Exchange(ref _reusableConnectionValueTaskSource, null)?.Dispose();
6670
}
6771
}
6872
}

src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Windows.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,25 @@ internal virtual void TryToReuse(PipeValueTaskSource source)
255255
if (source is ReadWriteValueTaskSource readWriteSource)
256256
{
257257
ref ReadWriteValueTaskSource? field = ref readWriteSource._isWrite ? ref _reusableWriteValueTaskSource : ref _reusableReadValueTaskSource;
258+
259+
// Try to return the instance. If there's already something in the field, then there had been concurrent
260+
// operations and someone else already returned their instance, so just dispose of this one (the "pool" has
261+
// a size of 1). If there's not something there and we're successful in storing our instance, the 99% case is
262+
// this is normal operation, there wasn't a concurrent operation, and we just successfully returned the rented
263+
// instance. However, it could also be because the stream has been disposed, in which case the disposal process
264+
// would have nulled out the field, and since this instance wasn't present when disposal happened, it won't have
265+
// been disposed. Further, disposal won't happen again, so just leaving the instance there will result in its
266+
// resources being leaked. Instead, _after_ returning the instance we then check whether the stream is now
267+
// disposed, and if it is, we then try to re-rent the instance and dispose of it. It's fine if the disposal happens
268+
// after we've stored the instance back and before we check the stream's state, as then this code will be racing
269+
// with disposal and only one of the two will succeed in exchanging out the instance.
258270
if (Interlocked.CompareExchange(ref field, readWriteSource, null) is not null)
259271
{
260-
source._preallocatedOverlapped.Dispose();
272+
source.Dispose();
273+
}
274+
else if (State == PipeState.Closed)
275+
{
276+
Interlocked.Exchange(ref field, null)?.Dispose();
261277
}
262278
}
263279
}

src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ public override Task FlushAsync(CancellationToken cancellationToken)
144144

145145
protected override void Dispose(bool disposing)
146146
{
147+
// Mark the pipe as closed before calling DisposeCore. That way, other threads that might
148+
// be synchronizing on shared resources disposed of in DisposeCore will be guaranteed to
149+
// see the closed state after that synchronization.
150+
_state = PipeState.Closed;
151+
147152
try
148153
{
149154
// Nothing will be done differently based on whether we are
@@ -159,8 +164,6 @@ protected override void Dispose(bool disposing)
159164
{
160165
base.Dispose(disposing);
161166
}
162-
163-
_state = PipeState.Closed;
164167
}
165168

166169
// ********************** Public Properties *********************** //

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ private void TryToReuse(OverlappedValueTaskSource source)
3535

3636
if (Interlocked.CompareExchange(ref _reusableOverlappedValueTaskSource, source, null) is not null)
3737
{
38-
source._preallocatedOverlapped.Dispose();
38+
source.Dispose();
39+
}
40+
else if (IsClosed)
41+
{
42+
Interlocked.Exchange(ref _reusableOverlappedValueTaskSource, null)?.Dispose();
3943
}
4044
}
4145

0 commit comments

Comments
 (0)