From 6daba4ec66ddb327e81a7341e9c4061a63fe8d19 Mon Sep 17 00:00:00 2001 From: XananasX7 Date: Thu, 3 Sep 2026 20:26:25 -0400 Subject: [PATCH] Fix gz_write() after a stall on a non-blocking destination 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. --- ChangeLog | 6 +++++- gzwrite.c | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 4e86c26883..6b755ed59d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,7 +2,11 @@ ChangeLog file for zlib Changes in 1.3.2.1 (xx Feb 2026) -- +- Fix gz_write() crash and corruption after a stall on a non-blocking + destination: input left pending in the caller's buffer by a partial + direct write was assumed to be in the internal input buffer, causing + an out-of-bounds copy on the next write, and could be compressed a + second time when the caller resubmitted it Changes in 1.3.2 (17 Feb 2026) - Continued rewrite of CMake build [Vollstrecker] diff --git a/gzwrite.c b/gzwrite.c index b5026e5fad..805701bc06 100644 --- a/gzwrite.c +++ b/gzwrite.c @@ -201,6 +201,22 @@ local z_size_t gz_write(gz_statep state, voidpc buf, z_size_t len) { if (state->skip && gz_zero(state) == -1) return 0; + /* If input is still pending from a previous write that stalled on a + non-blocking destination, and that pending input is not in the input + buffer, then it is in the caller's buffer, left there by a direct + (large) write. Such input was not counted as consumed when the + previous partial result was returned, so the caller will provide it + again, starting at the current position of the new call's buffer. + Since that pending input has not yet been given to deflate, nothing + is lost by dropping the pending reference to it here and letting + this call's input be compressed in its place. (Input pending in + the input buffer itself must be kept, as it was already counted as + consumed and is not being provided again.) */ + if (state->strm.avail_in && + (state->strm.next_in < state->in || + state->strm.next_in >= state->in + state->size)) + state->strm.avail_in = 0; + /* for small len, copy to input buffer, otherwise compress directly */ if (len < state->size) { /* copy to input buffer, compress when full */