Heap write past caller's buffer in gz_write()/gzputc() after non-blocking write stall
File: gzwrite.c, gz_write() small-write path, memcpy(state->in + have, buf, copy) at line ~217 (also gzputc() fast path, lines ~325-345; transparent "T" mode affected via the same path).
Introduced: with the non-blocking device support (same feature as #1256; first released in 1.3.1.2, also present in 1.3.2 and master).
This bug is different from issue #1256
Summary
gz_write() has two paths: a small-write path that copies into the internal input buffer state->in (2 * state->size bytes), and a direct path for len >= state->size that points strm->next_in at the caller's buffer and compresses from there. When a direct-path write stalls on EAGAIN (non-blocking fd, consumer not draining), gz_comp() returns -1 and leaves strm->next_in still inside the caller's buffer with strm->avail_in > 0. A subsequent small write then computes have via pointer arithmetic across the two unrelated objects and memcpy()s to a destination derived from it — past the caller's buffer, marching forward by len per call.
The buggy code
/* gz_write(), small-write path (len < state->size) */
for (;;) {
unsigned have, copy;
if (state->strm.avail_in == 0)
state->strm.next_in = state->in; /* reset only when empty */
have = (unsigned)((state->strm.next_in + state->strm.avail_in) -
state->in); /* cross-object subtraction */
copy = state->size - have;
if (copy > len)
copy = (unsigned)len;
memcpy(state->in + have, buf, copy); /* OOB destination */
...
/* direct path (len >= state->size): on stall, next_in/avail_in are left
pointing into the caller's buffer */
else {
if (state->strm.avail_in && gz_comp(state, Z_NO_FLUSH) == -1)
return 0;
state->strm.next_in = (z_const Bytef *)buf;
do {
...
ret = gz_comp(state, Z_NO_FLUSH);
n -= state->strm.avail_in;
len -= n;
if (ret == -1)
return state->again ? put - len : 0; /* stall: avail_in > 0 remains */
} while (len);
}
gz_comp() on EAGAIN returns -1 before touching the input: deflate() is not called, so strm->next_in and strm->avail_in are preserved exactly as the direct path left them — next_in inside the caller's buffer, avail_in > 0.
Why it overflows
The small path only resets next_in = state->in when avail_in == 0. In the stale state (avail_in > 0, next_in in the caller's buffer) it computes:
have = (unsigned)((next_in + avail_in) - state->in)
This is pointer subtraction across two unrelated allocations (undefined behavior; truncated to 32 bits). copy is then clamped to len, so the corruption is in the destination, not the length:
destination = state->in + have ≈ next_in + avail_in
i.e. just past the consumed region of the caller's buffer. Each subsequent small write advances the destination by len bytes (avail_in is inflated by the copied amount), marching beyond the caller's buffer and into adjacent heap, until an unmapped page is hit.
gzputc()'s fast path computes the identical have itself before falling back to gz_write(); with a garbage have < state->size it writes state->in[have] = c directly (out-of-bounds single byte); otherwise it reaches the same memcpy. Transparent mode ("T") is affected through the same code, since gz_comp()'s direct branch preserves the stale next_in on EAGAIN the same way.
Secondary primitive (read side)
With the stale state, a later successful flush (gzflush()/gzclose() once the consumer drains) makes deflate() consume from next_in — reading past the caller's buffer or from freed memory and emitting those bytes into the gzip output stream (out-of-bounds/freed-memory read leaked to the stream).
Trigger requirements
gzFile on a non-blocking descriptor (O_NONBLOCK; gzopen mode "N" or gzdopen of an already non-blocking fd).
- A
gzwrite() with len >= state->size (default 8192) of incompressible data (random/encrypted payloads) that stalls on EAGAIN — the stale state is only transiently reachable: if the pipe has any room, the direct-path entry drain resets it, so the pipe must be completely full when the stall occurs (depends on pipe capacity; macOS 16 KiB, Linux 64 KiB).
- A subsequent small write (
gzputc() or gzwrite() with len < state->size), which returns full success while corrupting memory.
Impact
- Heap write past the caller's buffer with attacker-influenced content, marching forward per call: heap corruption with RCE potential in processes that legitimately use non-blocking gz output (logging proxies, streaming services); deterministic DoS in all cases.
- The write destination resolves precisely to
next_in + avail_in only when the caller's buffer and state->in share the same high 32 address bits; otherwise the 32-bit truncation yields a random wild address (still a crash).
- The API returns success (positive counts) in the corrupt states, so callers cannot detect the condition.
Reproduction (ASan)
#include <zlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
int p[2];
pipe(p);
fcntl(p[1], F_SETFL, O_NONBLOCK);
gzFile gz = gzdopen(p[1], "w");
char big[65536]; /* < glibc mmap threshold: heap-local placement */
char small[64] = "AAAA";
int i;
for (i = 0; i < 65536; i++) big[i] = (char)rand(); /* incompressible */
{ /* pre-fill the pipe so the first big write stalls deterministically */
char filler[16384];
for (i = 0; i < 16384; i++) filler[i] = (char)rand();
while (write(p[1], filler, sizeof filler) > 0);
}
gzwrite(gz, big, 65536); /* stalls: next_in stays inside big, avail_in > 0 */
for (;;)
gzwrite(gz, small, 64); /* memcpy at next_in + avail_in, past big */
return 0;
}
ASan reports a heap-buffer-overflow (memcpy in gz_write, gzwrite.c:217) as the destination marches past big; with unlucky buffer placement the first report is a SEGV/BUS on a wild address.
Suggested fix
In the small path, restore the invariant before computing have: if strm->avail_in != 0 and strm->next_in is outside [state->in, state->in + 2*state->size), drain first via gz_comp(Z_NO_FLUSH) and return the partial/error count on failure — never fall through to memcpy. Apply the same guard in gzputc() before its own have computation. Do not simply reset next_in = state->in while avail_in > 0 (that would discard unconsumed data and corrupt the stream).
Related: #1256 (the gzvprintf() variant of the same non-blocking write-stall handling; this is a distinct bug in gz_write()/gzputc()).
Heap write past caller's buffer in gz_write()/gzputc() after non-blocking write stall
File:
gzwrite.c,gz_write()small-write path,memcpy(state->in + have, buf, copy)at line ~217 (alsogzputc()fast path, lines ~325-345; transparent"T"mode affected via the same path).Introduced: with the non-blocking device support (same feature as #1256; first released in 1.3.1.2, also present in 1.3.2 and master).
This bug is different from issue #1256
Summary
gz_write()has two paths: a small-write path that copies into the internal input bufferstate->in(2 * state->sizebytes), and a direct path forlen >= state->sizethat pointsstrm->next_inat the caller's buffer and compresses from there. When a direct-path write stalls onEAGAIN(non-blocking fd, consumer not draining),gz_comp()returns -1 and leavesstrm->next_instill inside the caller's buffer withstrm->avail_in > 0. A subsequent small write then computeshavevia pointer arithmetic across the two unrelated objects andmemcpy()s to a destination derived from it — past the caller's buffer, marching forward bylenper call.The buggy code
gz_comp()onEAGAINreturns -1 before touching the input:deflate()is not called, sostrm->next_inandstrm->avail_inare preserved exactly as the direct path left them —next_ininside the caller's buffer,avail_in > 0.Why it overflows
The small path only resets
next_in = state->inwhenavail_in == 0. In the stale state (avail_in > 0,next_inin the caller's buffer) it computes:This is pointer subtraction across two unrelated allocations (undefined behavior; truncated to 32 bits).
copyis then clamped tolen, so the corruption is in the destination, not the length:i.e. just past the consumed region of the caller's buffer. Each subsequent small write advances the destination by
lenbytes (avail_inis inflated by the copied amount), marching beyond the caller's buffer and into adjacent heap, until an unmapped page is hit.gzputc()'s fast path computes the identicalhaveitself before falling back togz_write(); with a garbagehave < state->sizeit writesstate->in[have] = cdirectly (out-of-bounds single byte); otherwise it reaches the samememcpy. Transparent mode ("T") is affected through the same code, sincegz_comp()'s direct branch preserves the stalenext_inonEAGAINthe same way.Secondary primitive (read side)
With the stale state, a later successful flush (
gzflush()/gzclose()once the consumer drains) makesdeflate()consume fromnext_in— reading past the caller's buffer or from freed memory and emitting those bytes into the gzip output stream (out-of-bounds/freed-memory read leaked to the stream).Trigger requirements
gzFileon a non-blocking descriptor (O_NONBLOCK;gzopenmode"N"orgzdopenof an already non-blocking fd).gzwrite()withlen >= state->size(default 8192) of incompressible data (random/encrypted payloads) that stalls onEAGAIN— the stale state is only transiently reachable: if the pipe has any room, the direct-path entry drain resets it, so the pipe must be completely full when the stall occurs (depends on pipe capacity; macOS 16 KiB, Linux 64 KiB).gzputc()orgzwrite()withlen < state->size), which returns full success while corrupting memory.Impact
next_in + avail_inonly when the caller's buffer andstate->inshare the same high 32 address bits; otherwise the 32-bit truncation yields a random wild address (still a crash).Reproduction (ASan)
ASan reports a heap-buffer-overflow (
memcpyingz_write, gzwrite.c:217) as the destination marches pastbig; with unlucky buffer placement the first report is a SEGV/BUS on a wild address.Suggested fix
In the small path, restore the invariant before computing
have: ifstrm->avail_in != 0andstrm->next_inis outside[state->in, state->in + 2*state->size), drain first viagz_comp(Z_NO_FLUSH)and return the partial/error count on failure — never fall through tomemcpy. Apply the same guard ingzputc()before its ownhavecomputation. Do not simply resetnext_in = state->inwhileavail_in > 0(that would discard unconsumed data and corrupt the stream).Related: #1256 (the
gzvprintf()variant of the same non-blocking write-stall handling; this is a distinct bug ingz_write()/gzputc()).