Skip to content

Fix race between zlib Inflate/Deflate and Dispose#128752

Open
rzikm wants to merge 2 commits into
mainfrom
rzikm/fix-zlib-dispose-race
Open

Fix race between zlib Inflate/Deflate and Dispose#128752
rzikm wants to merge 2 commits into
mainfrom
rzikm/fix-zlib-dispose-race

Conversation

@rzikm
Copy link
Copy Markdown
Member

@rzikm rzikm commented May 29, 2026

Fixes #128550.

Inflater/Deflater synchronize their native zlib calls via a managed lock, but Dispose is not synchronized, so a concurrent Dispose on one thread could run InflateEnd/DeflateEnd and free the underlying z_stream while another thread was mid-call inside the native inflate/deflate, leading to use-after-free crashes.

The fix moves the protection down into ZLibStreamHandle. Inflate, InflateReset2_, and Deflate now bracket their native invocations with DangerousAddRef/DangerousRelease, which is exactly what SafeHandle is designed for: it defers ReleaseHandle (and therefore the InflateEnd/DeflateEnd P/Invoke) until all in-flight callers have released their refs. The redundant EnsureNotDisposed check is removed from these methods since DangerousAddRef already throws ObjectDisposedException for a closed handle.

The other ZLibStreamHandle members (DeflateEnd, InflateEnd, InflateInit2_, DeflateInit2_) are intentionally left as-is: they only run from initialization or from ReleaseHandle/Dispose paths that are not the racing party.

Hold a SafeHandle ref via DangerousAddRef/Release around the native Inflate, InflateReset2_, and Deflate calls so a concurrent Dispose cannot run InflateEnd/DeflateEnd and free the zlib state from underneath an in-flight operation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 29, 2026 10:37
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the shared zlib stream handle used by System.IO.Compression and System.Net.WebSockets so concurrent disposal cannot release native zlib state while an inflate/deflate native call is in flight.

Changes:

  • Wraps Deflate, InflateReset2_, and Inflate native calls with DangerousAddRef/DangerousRelease.
  • Relies on SafeHandle ref-counting instead of the previous disposed-state check at these call sites.

Comment thread src/libraries/Common/src/System/IO/Compression/ZLibNative.cs
@rzikm rzikm requested a review from a team May 29, 2026 11:16
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

@github-actions
Copy link
Copy Markdown
Contributor

Note

This review was generated by Copilot.

🤖 Copilot Code Review — PR #128752

Holistic Assessment

Motivation: The PR fixes a real use-after-free race (issue #128550) where concurrent Dispose frees the native z_stream while Inflate/Deflate is in-flight. The bug is valid and the fix is warranted.

Approach: DangerousAddRef/DangerousRelease is the canonical SafeHandle pattern for exactly this scenario — deferring ReleaseHandle until all in-flight native calls complete. This is the right fix at the right layer.

Summary: ✅ LGTM. The change is minimal, correctly scoped, and applies the idiomatic SafeHandle ref-counting pattern. No blocking issues found.


Detailed Findings

✅ Correctness — DangerousAddRef/DangerousRelease pattern is correct

All three protected methods (Deflate, Inflate, InflateReset2_) follow the correct pattern:

  1. bool refAdded = false declared before try
  2. DangerousAddRef(ref refAdded) as first statement in try
  3. DangerousRelease() in finally, guarded by if (refAdded)

This ensures ReleaseHandle (and therefore DeflateEnd/InflateEnd) cannot execute while any of these calls are in-flight.

✅ Removal of EnsureNotDisposed — correct

DangerousAddRef already throws ObjectDisposedException when the handle is closed/disposed, making the explicit EnsureNotDisposed check redundant. Removing it from DeflateEnd/InflateEnd is also fine since those are only called from ReleaseHandle (which runs after all refs are released) or during initialization failure cleanup.

✅ Scope — unprotected methods are correctly identified

  • DeflateInit2_/InflateInit2_ run only during construction before the handle is shared
  • DeflateEnd/InflateEnd run only from ReleaseHandle, which is itself deferred by the ref count

These don't need protection.

✅ Cross-cutting — WebSocket callers also benefit

WebSocketDeflater and WebSocketInflater call the same ZLibStreamHandle.Deflate/Inflate methods. They have similar Dispose patterns (non-synchronized _stream?.Dispose()). This fix protects those callers as well without requiring changes there.

💡 Observation — EnsureState ordering

EnsureState is called after DangerousAddRef, so if it throws, the ref is still correctly released in the finally block. The ordering is sound.

Generated by Code Review for issue #128752 · ● 2M ·

EnsureNotDisposed();
EnsureState(State.InitializedForInflate);

fixed (ZStream* stream = &_zStream)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does the fix need to be applied here as well?

EnsureNotDisposed();
EnsureState(State.InitializedForDeflate);

fixed (ZStream* stream = &_zStream)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And here

fixed (ZStream* stream = &_zStream)
fixed (ZStream* stream = &_zStream)
{
return Interop.ZLib.Deflate(stream, flush);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the Zlib implementation robust against multiple threads calling Deflate at the same time on the same state? It is fine for it to produce corrupted results, but it is not fine for it to crash or corrupt memory that it does not own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent read and dispose zlib race condition

4 participants