Skip to content

Reliable clean-up of SafeHandles in Deflater/Inflater #114826

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

edwardneal
Copy link
Contributor

This reimplements #71991 in Deflater and Inflater. The PR was previously reverted in #85001 as a result of issue #84994.

As noted in the reverting PR, Deflater simply needed to dispose of the returned SafeHandle and supress its own finalizer if CreateZLibStreamForDeflate threw an exception. We now do this.

We couldn't do this for Inflater at the time - it had to support concatenated GZip data, and as part of that support it would recreate its ZLibStreamHandle. As a result of #113587, this recreation no longer happens and we can treat Inflater as we would treat Deflater. I've refactored Inflater.InflateInit out of existence and made _zlibStream readonly to reflect this.

This also incorporates a change to both class' DeallocateBufferHandle methods (when called by Dispose) to prevent them dereferencing a ZLibStreamHandle after it's been disposed of. This is roughly how #84994 was originally discovered.

If the call to CreateZLibStreamForXXflate throws an exception from the constructor, explicitly dispose of the ZLibStreamHandle (rather than relying on the finalizer to do it.)

Also incorporates a change to DeallocateBufferHandle (called from the Dispose path) to prevent it using a ZlibStreamHandle after it's been Disposed.
This aligns Inflater with Deflater.
Removed now-unnecessary usings.
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Apr 18, 2025
Copy link
Contributor

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


DeallocateInputBufferHandle();
// Unpin the input buffer, but avoid modifying the ZLibStreamHandle (which may have been disposed of.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Unpin the input buffer, but avoid modifying the ZLibStreamHandle (which may have been disposed of.)
// Unpin the input buffer, but avoid modifying the ZLibStreamHandle (which may have been disposed of).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid modifying the ZLibStreamHandle (which may have been disposed of.)

Why would setting those fields on the disposed instance be a problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem yet - but that's largely because ZLibStreamHandle's underlying handle field isn't used properly. Setting these properties on an ZLibStreamHandle instance sets fields on a ZStream struct variable inside the instance; calling InflateInit2_ fixes that variable in memory and passes a pointer to it to the underlying PInvoke. At the moment, setting a property on the disposed instance will always work.

There's a bug open to fix those handle semantics (#89445), and I'm testing a few approaches. Most of them involve allocating and freeing native memory though - at which point, setting properties on a disposed ZLibStreamHandle instance becomes a use-after-free bug.

{
_zlibStream.AvailIn = 0;
_zlibStream.NextIn = ZLibNative.ZNullPtr;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

errC = ZLibNative.CreateZLibStreamForDeflate(out _zlibStream, compressionLevel, windowBits, memLevel, strategy);
errC = ZLibNative.CreateZLibStreamForDeflate(out zlibStream, compressionLevel, windowBits, memLevel, strategy);

_zlibStream = zlibStream;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this dance should be done here. CreateZLibStreamForDeflate should only be setting the out once it's successful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a speculative commit so we can see roughly what that looks like.

We could turn CreateZLibStreamForXxflate into TryCreateZLibStreamForXxflate and add the right nullability annotations. It's the smallest change at the call sites and it'd simplify the nullability dance but introduce a new one as we shuffle the error handling state around. I'd prefer to avoid it.

I've currently folded most of the error handling into CreateZLibStreamForXxflate, which will either return a non-null ZLibStreamHandle or throw. This simplifies the System.IO.Compression call sites slightly. ZLibNative.cs is also source-included in System.Net.WebSockets though, which means that we can't throw ZLibException directly (it's excluded from the System.IO.Compression ref assembly as per 112440) and have to throw a new shim exception class.

More broadly, I was planning to split the ZLibStreamHandle class into separate ZLibStreamInflateHandle and ZLibStreamDeflateHandle classes; the CreateZLibStreamForXxflate methods would just become constructors. I'm inclined to roll most of the work from this commit into that future PR, and keep this one limited to the finalization fixes. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.IO.Compression community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants