Skip to content

Commit a1a653c

Browse files
authored
Add missing GC.SuppressFinalize (#64997)
1 parent ce19375 commit a1a653c

File tree

6 files changed

+250
-64
lines changed

6 files changed

+250
-64
lines changed

src/libraries/System.IO.FileSystem/tests/FileStream/Dispose.cs

Lines changed: 223 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.Win32.SafeHandles;
5-
using System;
6-
using System.Diagnostics;
7-
using System.IO;
85
using Microsoft.DotNet.RemoteExecutor;
96
using Xunit;
7+
using System.Collections.Generic;
8+
using System.Threading.Tasks;
109

1110
namespace System.IO.Tests
1211
{
12+
[Collection(nameof(DisableParallelization))] // make sure no other tests are calling GC.Collect()
1313
public class FileStream_Dispose : FileSystemTest
1414
{
1515
[Fact]
1616
public void DisposeClosesHandle()
1717
{
1818
SafeFileHandle handle;
19-
using(FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create))
19+
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create))
2020
{
2121
handle = fs.SafeFileHandle;
2222
}
@@ -199,7 +199,7 @@ public void Finalizer_CallsVirtualDispose_FalseArg()
199199
public void DisposeFlushesWriteBuffer()
200200
{
201201
string fileName = GetTestFilePath();
202-
using(FileStream fs = new FileStream(fileName, FileMode.Create))
202+
using (FileStream fs = new FileStream(fileName, FileMode.Create))
203203
{
204204
fs.Write(TestBuffer, 0, TestBuffer.Length);
205205
}
@@ -213,20 +213,143 @@ public void DisposeFlushesWriteBuffer()
213213
}
214214
}
215215

216+
public class DerivedFileStreamWithFinalizer : FileStream
217+
{
218+
public static int DisposeTrueCalled = 0;
219+
public static int DisposeFalseCalled = 0;
220+
221+
public DerivedFileStreamWithFinalizer(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
222+
: base(path, mode, access, share, bufferSize, options)
223+
{
224+
}
225+
226+
public DerivedFileStreamWithFinalizer(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync)
227+
: base(handle, access, bufferSize, isAsync)
228+
{
229+
}
230+
231+
public DerivedFileStreamWithFinalizer(IntPtr handle, FileAccess access, bool ownsHandle)
232+
#pragma warning disable CS0618 // Type or member is obsolete
233+
: base(handle, access, ownsHandle)
234+
#pragma warning restore CS0618 // Type or member is obsolete
235+
{
236+
}
237+
238+
~DerivedFileStreamWithFinalizer() => Dispose(false);
239+
240+
protected override void Dispose(bool disposing)
241+
{
242+
if (disposing)
243+
{
244+
DisposeTrueCalled++;
245+
}
246+
else
247+
{
248+
DisposeFalseCalled++;
249+
}
250+
251+
base.Dispose(disposing);
252+
}
253+
}
254+
255+
public class DerivedFileStreamWithoutFinalizer : FileStream
256+
{
257+
public static int DisposeTrueCalled = 0;
258+
public static int DisposeFalseCalled = 0;
259+
260+
public DerivedFileStreamWithoutFinalizer(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
261+
: base(path, mode, access, share, bufferSize, options)
262+
{
263+
}
264+
265+
public DerivedFileStreamWithoutFinalizer(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync)
266+
: base(handle, access, bufferSize, isAsync)
267+
{
268+
}
269+
270+
public DerivedFileStreamWithoutFinalizer(IntPtr handle, FileAccess access, bool ownsHandle)
271+
#pragma warning disable CS0618 // Type or member is obsolete
272+
: base(handle, access, ownsHandle)
273+
#pragma warning restore CS0618 // Type or member is obsolete
274+
{
275+
}
276+
277+
protected override void Dispose(bool disposing)
278+
{
279+
if (disposing)
280+
{
281+
DisposeTrueCalled++;
282+
}
283+
else
284+
{
285+
DisposeFalseCalled++;
286+
}
287+
288+
base.Dispose(disposing);
289+
}
290+
}
291+
216292
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
217293
public void FinalizeFlushesWriteBuffer()
294+
=> VerifyFlushedBufferOnFinalization(
295+
filePath => new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, useAsync: false));
296+
297+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
298+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromPath()
299+
=> VerifyFlushedBufferOnFinalization(
300+
filePath => new DerivedFileStreamWithFinalizer(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, FileOptions.None));
301+
302+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
303+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromSafeFileHandle()
304+
=> VerifyFlushedBufferOnFinalization(
305+
filePath => new DerivedFileStreamWithFinalizer(
306+
File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, FileOptions.None),
307+
FileAccess.Write, bufferSize: 4096, isAsync: false));
308+
309+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
310+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithFinalizerCreatedFromIntPtr()
311+
=> VerifyFlushedBufferOnFinalization(
312+
filePath => new DerivedFileStreamWithFinalizer(
313+
File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, FileOptions.None).DangerousGetHandle(),
314+
FileAccess.Write,
315+
ownsHandle: true));
316+
317+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
318+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromPath()
319+
=> VerifyFlushedBufferOnFinalization(
320+
filePath => new DerivedFileStreamWithoutFinalizer(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, FileOptions.None));
321+
322+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
323+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromSafeFileHandle()
324+
=> VerifyFlushedBufferOnFinalization(
325+
filePath => new DerivedFileStreamWithoutFinalizer(
326+
File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, FileOptions.None),
327+
FileAccess.Write, bufferSize: 4096, isAsync: false));
328+
329+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
330+
public void FinalizeFlushesWriteBufferForDerivedFileStreamWithoutFinalizerCreatedFromIntPtr()
331+
=> VerifyFlushedBufferOnFinalization(
332+
filePath => new DerivedFileStreamWithoutFinalizer(
333+
File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, FileOptions.None).DangerousGetHandle(),
334+
FileAccess.Write,
335+
ownsHandle: true));
336+
337+
private void VerifyFlushedBufferOnFinalization(Func<string, FileStream> factory)
218338
{
219339
string fileName = GetTestFilePath();
220340

221341
// use a separate method to be sure that fs isn't rooted at time of GC.
222-
Action leakFs = () =>
342+
Func<string, Type> leakFs = filePath =>
223343
{
224344
// we must specify useAsync:false, otherwise the finalizer just kicks off an async write.
225-
FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, useAsync: false);
345+
FileStream fs = factory(filePath);
226346
fs.Write(TestBuffer, 0, TestBuffer.Length);
227-
fs = null;
347+
return fs.GetType();
228348
};
229-
leakFs();
349+
350+
int beforeWithFinalizer = DerivedFileStreamWithFinalizer.DisposeFalseCalled;
351+
int beforeWithoutFinalizer = DerivedFileStreamWithoutFinalizer.DisposeFalseCalled;
352+
Type type = leakFs(fileName);
230353

231354
GC.Collect();
232355
GC.WaitForPendingFinalizers();
@@ -238,6 +361,97 @@ public void FinalizeFlushesWriteBuffer()
238361
fsr.Read(buffer, 0, buffer.Length);
239362
Assert.Equal(TestBuffer, buffer);
240363
}
364+
365+
if (type == typeof(DerivedFileStreamWithFinalizer))
366+
{
367+
// derived type finalizer implicitly calls base type finalizer, hence +2
368+
Assert.Equal(beforeWithFinalizer + 2, DerivedFileStreamWithFinalizer.DisposeFalseCalled);
369+
Assert.Equal(0, DerivedFileStreamWithFinalizer.DisposeTrueCalled);
370+
}
371+
else if (type == typeof(DerivedFileStreamWithoutFinalizer))
372+
{
373+
Assert.Equal(beforeWithoutFinalizer + 1, DerivedFileStreamWithoutFinalizer.DisposeFalseCalled);
374+
Assert.Equal(0, DerivedFileStreamWithoutFinalizer.DisposeTrueCalled);
375+
}
376+
}
377+
378+
// this type exists so DerivedFileStreamStrategy can be tested as well
379+
public class DerivedFileStream : FileStream
380+
{
381+
public DerivedFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
382+
: base(path, mode, access, share, bufferSize, options)
383+
{
384+
}
385+
}
386+
387+
public static IEnumerable<object[]> GetFileStreamDisposeSuppressesStrategyFinalizationArgs()
388+
{
389+
foreach (int bufferSize in new[] { 1, 4096 })
390+
{
391+
foreach (FileOptions fileOptions in new[] { FileOptions.Asynchronous, FileOptions.None })
392+
{
393+
yield return new object[] { bufferSize, fileOptions };
394+
}
395+
}
396+
}
397+
398+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
399+
[MemberData(nameof(GetFileStreamDisposeSuppressesStrategyFinalizationArgs))]
400+
public Task DisposeSuppressesStrategyFinalization(int bufferSize, FileOptions options)
401+
=> VerifyStrategyFinalization(
402+
() => new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, options | FileOptions.DeleteOnClose),
403+
false);
404+
405+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
406+
[MemberData(nameof(GetFileStreamDisposeSuppressesStrategyFinalizationArgs))]
407+
public Task DisposeSuppressesStrategyFinalizationAsync(int bufferSize, FileOptions options)
408+
=> VerifyStrategyFinalization(
409+
() => new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, options | FileOptions.DeleteOnClose),
410+
true);
411+
412+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
413+
[MemberData(nameof(GetFileStreamDisposeSuppressesStrategyFinalizationArgs))]
414+
public Task DerivedFileStreamDisposeSuppressesStrategyFinalization(int bufferSize, FileOptions options)
415+
=> VerifyStrategyFinalization(
416+
() => new DerivedFileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, options | FileOptions.DeleteOnClose),
417+
false);
418+
419+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsPreciseGcSupported))]
420+
[MemberData(nameof(GetFileStreamDisposeSuppressesStrategyFinalizationArgs))]
421+
public Task DerivedFileStreamDisposeSuppressesStrategyFinalizationAsync(int bufferSize, FileOptions options)
422+
=> VerifyStrategyFinalization(
423+
() => new DerivedFileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, options | FileOptions.DeleteOnClose),
424+
true);
425+
426+
private static async Task VerifyStrategyFinalization(Func<FileStream> factory, bool useAsync)
427+
{
428+
WeakReference weakReference = await EnsureFileStreamIsNotRooted(factory, useAsync);
429+
Assert.True(weakReference.IsAlive);
430+
GC.Collect();
431+
Assert.False(weakReference.IsAlive);
432+
433+
// separate method to avoid JIT lifetime-extension issues
434+
static async Task<WeakReference> EnsureFileStreamIsNotRooted(Func<FileStream> factory, bool useAsync)
435+
{
436+
FileStream fs = factory();
437+
WeakReference weakReference = new WeakReference(
438+
(Stream)typeof(FileStream)
439+
.GetField("_strategy", Reflection.BindingFlags.NonPublic | Reflection.BindingFlags.Instance)
440+
.GetValue(fs),
441+
trackResurrection: true);
442+
443+
Assert.True(weakReference.IsAlive);
444+
445+
if (useAsync)
446+
{
447+
await fs.DisposeAsync();
448+
}
449+
{
450+
fs.Dispose();
451+
}
452+
453+
return weakReference;
454+
}
241455
}
242456
}
243457
}

src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share
147147
{
148148
}
149149

150+
~FileStream()
151+
{
152+
// Preserved for compatibility since FileStream has defined a
153+
// finalizer in past releases and derived classes may depend
154+
// on Dispose(false) call.
155+
Dispose(false);
156+
}
157+
150158
/// <summary>
151159
/// Initializes a new instance of the <see cref="System.IO.FileStream" /> class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size.
152160
/// </summary>
@@ -486,9 +494,8 @@ public override long Position
486494
/// <param name="value">The byte to write to the stream.</param>
487495
public override void WriteByte(byte value) => _strategy.WriteByte(value);
488496

489-
protected override void Dispose(bool disposing) => _strategy.DisposeInternal(disposing);
490-
491-
internal void DisposeInternal(bool disposing) => Dispose(disposing);
497+
// _strategy can be null only when ctor has thrown
498+
protected override void Dispose(bool disposing) => _strategy?.DisposeInternal(disposing);
492499

493500
public override ValueTask DisposeAsync() => _strategy.DisposeAsync();
494501

src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,6 @@ internal BufferedFileStreamStrategy(FileStreamStrategy strategy, int bufferSize)
3131
_bufferSize = bufferSize;
3232
}
3333

34-
~BufferedFileStreamStrategy()
35-
{
36-
try
37-
{
38-
// the finalizer must at least try to flush the write buffer
39-
// so we enforce it by passing always true
40-
Dispose(true);
41-
}
42-
catch (Exception e) when (FileStreamHelpers.IsIoRelatedException(e))
43-
{
44-
// On finalization, ignore failures from trying to flush the write buffer,
45-
// e.g. if this stream is wrapping a pipe and the pipe is now broken.
46-
}
47-
}
48-
4934
public override bool CanRead => _strategy.CanRead;
5035

5136
public override bool CanWrite => _strategy.CanWrite;
@@ -137,34 +122,32 @@ public override async ValueTask DisposeAsync()
137122
}
138123
}
139124

140-
internal override void DisposeInternal(bool disposing) => Dispose(disposing);
141-
142-
protected override void Dispose(bool disposing)
125+
protected sealed override void Dispose(bool disposing)
143126
{
127+
if (_strategy.IsClosed)
128+
{
129+
return;
130+
}
131+
144132
try
145133
{
146-
if (disposing && !_strategy.IsClosed)
147-
{
148-
try
149-
{
150-
Flush();
151-
}
152-
finally
153-
{
154-
_strategy.Dispose();
155-
}
156-
}
134+
Flush();
135+
}
136+
catch (Exception e) when (!disposing && FileStreamHelpers.IsIoRelatedException(e))
137+
{
138+
// On finalization, ignore failures from trying to flush the write buffer,
139+
// e.g. if this stream is wrapping a pipe and the pipe is now broken.
157140
}
158141
finally
159142
{
160143
// Don't set the buffer to null, to avoid a NullReferenceException
161144
// when users have a race condition in their code (i.e. they call
162145
// FileStream.Close when calling another method on FileStream like Read).
163146

164-
// Call base.Dispose(bool) to cleanup async IO resources
165-
base.Dispose(disposing);
166-
147+
// There is no need to call base.Dispose as it's empty
167148
_writePos = 0;
149+
150+
_strategy.DisposeInternal(disposing);
168151
}
169152
}
170153

0 commit comments

Comments
 (0)