Fix race between zlib Inflate/Deflate and Dispose#128752
Conversation
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>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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_, andInflatenative calls withDangerousAddRef/DangerousRelease. - Relies on SafeHandle ref-counting instead of the previous disposed-state check at these call sites.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @karelz, @dotnet/area-system-io-compression |
|
Note This review was generated by Copilot. 🤖 Copilot Code Review — PR #128752Holistic AssessmentMotivation: The PR fixes a real use-after-free race (issue #128550) where concurrent Approach: Summary: ✅ LGTM. The change is minimal, correctly scoped, and applies the idiomatic Detailed Findings✅ Correctness — DangerousAddRef/DangerousRelease pattern is correctAll three protected methods (
This ensures ✅ Removal of EnsureNotDisposed — correct
✅ Scope — unprotected methods are correctly identified
These don't need protection. ✅ Cross-cutting — WebSocket callers also benefit
💡 Observation — EnsureState ordering
|
| EnsureNotDisposed(); | ||
| EnsureState(State.InitializedForInflate); | ||
|
|
||
| fixed (ZStream* stream = &_zStream) |
There was a problem hiding this comment.
Does the fix need to be applied here as well?
| EnsureNotDisposed(); | ||
| EnsureState(State.InitializedForDeflate); | ||
|
|
||
| fixed (ZStream* stream = &_zStream) |
| fixed (ZStream* stream = &_zStream) | ||
| fixed (ZStream* stream = &_zStream) | ||
| { | ||
| return Interop.ZLib.Deflate(stream, flush); |
There was a problem hiding this comment.
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.
Fixes #128550.
Inflater/Deflatersynchronize their native zlib calls via a managedlock, butDisposeis not synchronized, so a concurrentDisposeon one thread could runInflateEnd/DeflateEndand free the underlyingz_streamwhile another thread was mid-call inside the nativeinflate/deflate, leading to use-after-free crashes.The fix moves the protection down into
ZLibStreamHandle.Inflate,InflateReset2_, andDeflatenow bracket their native invocations withDangerousAddRef/DangerousRelease, which is exactly whatSafeHandleis designed for: it defersReleaseHandle(and therefore theInflateEnd/DeflateEndP/Invoke) until all in-flight callers have released their refs. The redundantEnsureNotDisposedcheck is removed from these methods sinceDangerousAddRefalready throwsObjectDisposedExceptionfor a closed handle.The other
ZLibStreamHandlemembers (DeflateEnd,InflateEnd,InflateInit2_,DeflateInit2_) are intentionally left as-is: they only run from initialization or fromReleaseHandle/Disposepaths that are not the racing party.