Fix gz_write() crash and data duplication after a stall on a non-blocking destination - #1307
Open
XananasX7 wants to merge 1 commit into
Open
Fix gz_write() crash and data duplication after a stall on a non-blocking destination#1307XananasX7 wants to merge 1 commit into
XananasX7 wants to merge 1 commit into
Conversation
When a large gzwrite() to a non-blocking destination stalls (EAGAIN), gz_write() returns the number of bytes consumed with the remaining input still pending in the caller's buffer (strm.next_in points into that buffer, not into state->in). A subsequent small write (gzwrite, gzputs, gzputc, gzfwrite, gzprintf) then computed how much room was left in the input buffer as the difference between the foreign next_in pointer and state->in, producing a huge 'have' value, an underflowed 'copy' size, and a memcpy to a wild address in gzwrite.c. In the natural retry pattern the same pending bytes were also compressed a second time once the destination drained, silently duplicating data in the output stream. The pending input was never provided to deflate() and was not counted as consumed, so the caller resubmits it; drop the stale pending reference in gz_write() and let the new buffer be compressed in its place. Input pending in the internal input buffer is unaffected: it was already counted as consumed and is not resubmitted.
XananasX7
force-pushed
the
fix-gz-write-nonblocking-stall
branch
from
September 4, 2026 01:53
b3271eb to
6daba4e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gz_write()crashes (and can corrupt memory) when a write to a non-blocking destination stalls and is resumed. Triggered by the documented usage pattern for the'N'mode added in 1.3.2:gzwrite()returns a partial count onEAGAIN, and the caller continues with subsequentgz*calls.Root cause
When a large write takes the direct-compression path in
gz_write()(gzwrite.c,elsebranch),strm->next_inis pointed at the caller's buffer. Ifgz_comp()stalls withEAGAIN, it returns-1withstrm->avail_in > 0andstrm->next_instill inside the caller's buffer. Those bytes were not counted as consumed (the partial return excludes them), so the caller will resubmit them.The small (
len < state->size) buffered path assumes any pending input lives in the internal input buffer (state->in):Since
next_inpoints into the caller's buffer (a completely unrelated address),haveis a huge bogus offset and thememcpywrites to a wild address — deterministic SEGV under ASan and on real builds.The natural retry loop also silently duplicates data: once the destination drains, the next large write first pre-compresses the still-pending bytes (
gzwrite.c,if (state->strm.avail_in && gz_comp(...) == -1)) and then compresses the caller's resubmitted buffer, which starts at exactly those same bytes — encoding them twice.Fix
The pending bytes were never given to
deflate()and were not counted as consumed, so the caller always resubmits them.gz_write()now drops that stale pending reference when it is not in the internal input buffer and lets the new buffer be compressed in its place. Input pending in the internal buffer is untouched: it was already counted as consumed and is not resubmitted.Verification (ASan + UBSan, clang 14, unmodified 1.3.2-era develop HEAD)
gzwritestalls on full socket, thengzputsgz_writegzwrite.c:217gzputssucceedsoff += gzwrite(...)) draining peergzflush(Z_FINISH)retried untilZ_OK, thengzclose"w"and"wN"on regular filesmake testpasses on the patched tree.Minimal reproducer