Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add missing GC.SuppressFinalize(this) to FileStream.DisposeAsync #65899

Merged
merged 6 commits into from
Mar 1, 2022

Conversation

adamsitnik
Copy link
Member

I was unable to repro #65835 locally for a few hours, but I believe that the it's caused by lack of GC.SuppressFinalize(this) in FileStream.DisposeAsync and my recent changes from #64997 have just exposed the problem by adding the finalizer to FileStream itself.

Explanation: the default Stream.DisposeAsync calls Dispose:

public virtual ValueTask DisposeAsync()
{
try
{
Dispose();

which calls Close which calls GC.SuppressFinalize(this):

public void Dispose() => Close();
public virtual void Close()
{
// When initially designed, Stream required that all cleanup logic went into Close(),
// but this was thought up before IDisposable was added and never revisited. All subclasses
// should put their cleanup now in Dispose(bool).
Dispose(true);
GC.SuppressFinalize(this);
}

FileStream was overriding DisposeAsync, but not calling GC.SuppressFinalize(this).

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

In #65835 we can see that the buffer was actually written to disk twice. I guess (I was not able to repro it) that it's caused by a flush implemented for finalizer. I am not 100% sure because the finally block sets write position to 0:

finally
{
// Don't set the buffer to null, to avoid a NullReferenceException
// when users have a race condition in their code (i.e. they call
// FileStream.Close when calling another method on FileStream like Read).
_writePos = 0; // WriteByte hot path relies on this

So after DisposeAsync the flushing should in theory see that there is nothing to flush. Moreover, it should also observe that the handle was already closed.

protected sealed override void Dispose(bool disposing)
{
if (_strategy.IsClosed)
{
return;
}

I've tried really hard to write a failing unit test, but I've failed. The reason for that is that all custom types deriving from FileStream are calling BaseDisposeAsync:

internal ValueTask BaseDisposeAsync() => base.DisposeAsync();

which does call the base impl and is bug free.

@adamsitnik adamsitnik added this to the 7.0.0 milestone Feb 25, 2022
@ghost ghost assigned adamsitnik Feb 25, 2022
@ghost
Copy link

ghost commented Feb 25, 2022

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

I was unable to repro #65835 locally for a few hours, but I believe that the it's caused by lack of GC.SuppressFinalize(this) in FileStream.DisposeAsync and my recent changes from #64997 have just exposed the problem by adding the finalizer to FileStream itself.

Explanation: the default Stream.DisposeAsync calls Dispose:

public virtual ValueTask DisposeAsync()
{
try
{
Dispose();

which calls Close which calls GC.SuppressFinalize(this):

public void Dispose() => Close();
public virtual void Close()
{
// When initially designed, Stream required that all cleanup logic went into Close(),
// but this was thought up before IDisposable was added and never revisited. All subclasses
// should put their cleanup now in Dispose(bool).
Dispose(true);
GC.SuppressFinalize(this);
}

FileStream was overriding DisposeAsync, but not calling GC.SuppressFinalize(this).

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

In #65835 we can see that the buffer was actually written to disk twice. I guess (I was not able to repro it) that it's caused by a flush implemented for finalizer. I am not 100% sure because the finally block sets write position to 0:

finally
{
// Don't set the buffer to null, to avoid a NullReferenceException
// when users have a race condition in their code (i.e. they call
// FileStream.Close when calling another method on FileStream like Read).
_writePos = 0; // WriteByte hot path relies on this

So after DisposeAsync the flushing should in theory see that there is nothing to flush. Moreover, it should also observe that the handle was already closed.

protected sealed override void Dispose(bool disposing)
{
if (_strategy.IsClosed)
{
return;
}

I've tried really hard to write a failing unit test, but I've failed. The reason for that is that all custom types deriving from FileStream are calling BaseDisposeAsync:

internal ValueTask BaseDisposeAsync() => base.DisposeAsync();

which does call the base impl and is bug free.

Author: adamsitnik
Assignees: -
Labels:

area-System.IO

Milestone: 7.0.0

@adamsitnik adamsitnik merged commit 3cbed4b into dotnet:main Mar 1, 2022
@adamsitnik adamsitnik deleted the issue65835 branch March 1, 2022 13:15
@ghost ghost locked as resolved and limited conversation to collaborators Mar 31, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants