From e1f8d98aead142a5ff7c63393f559a811dbc091d Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 01:59:26 +0800 Subject: [PATCH 1/3] fix: avoid deadlock on Read after WriteTo drain After WriteTo/io.Copy drained the stream, a subsequent Read blocked forever trying to return a nil buffer to a full blockPool. Short-circuit Read when lastBlock is set and current is empty, and only return buffers with capacity to the pool. Also ensure WriteTo never returns io.EOF so io.Copy does not surface it (#38). Fixes #39 Fixes #38 --- gunzip.go | 38 ++++++++++++++++++++++---------- gunzip_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/gunzip.go b/gunzip.go index f4e4436..6e365eb 100644 --- a/gunzip.go +++ b/gunzip.go @@ -473,6 +473,11 @@ func (z *Reader) Read(p []byte) (n int, err error) { if len(p) == 0 { return 0, nil } + // Already drained (e.g. via WriteTo/io.Copy). Returning here avoids + // blocking on a full blockPool when current is nil. + if z.lastBlock && len(z.current) == 0 { + return 0, io.EOF + } if z.readAheadStarted.CompareAndSwap(false, true) { z.doReadAhead() @@ -504,8 +509,10 @@ func (z *Reader) Read(p []byte) (n int, err error) { if len(p) >= len(avail) { // If len(p) >= len(current), return all content of current n = copy(p, avail) - z.blockPool <- z.current - z.current = nil + if z.current != nil { + z.blockPool <- z.current + z.current = nil + } if z.lastBlock { err = io.EOF break @@ -555,20 +562,27 @@ func (z *Reader) WriteTo(w io.Writer) (n int64, err error) { } if read.err == io.EOF { z.lastBlock = true - err = nil } } - // Write what we got - n, err := w.Write(read.b) - if n != len(read.b) { - return total, io.ErrShortWrite + // Write what we got (even empty final block). + if len(read.b) > 0 { + n, err := w.Write(read.b) + if n != len(read.b) { + return total, io.ErrShortWrite + } + total += int64(n) + if err != nil { + return total, err + } } - total += int64(n) - if err != nil { - return total, err + // Put block back when it came from the pool. + if cap(read.b) > 0 { + z.blockPool <- read.b } - // Put block back - z.blockPool <- read.b + } + // WriterTo must not return io.EOF; io.Copy surfaces it to callers (#38). + if z.err == io.EOF { + return total, nil } return total, z.err } diff --git a/gunzip_test.go b/gunzip_test.go index 70e7258..e81618f 100644 --- a/gunzip_test.go +++ b/gunzip_test.go @@ -847,3 +847,63 @@ func TestWriterTo(t *testing.T) { t.Log("Size", n, "Checksum OK") }) } + + +func TestReadAfterWriteToNoDeadlock(t *testing.T) { + // echo hello | gzip -c + gzipData := []byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0x48, + 0xcd, 0xc9, 0xc9, 0xe7, 0x02, 0x00, 0x20, 0x30, 0x3a, 0x36, 0x06, 0x00, + 0x00, 0x00, + } + rdr, err := NewReader(bytes.NewReader(gzipData)) + if err != nil { + t.Fatal(err) + } + defer rdr.Close() + + n, err := io.Copy(io.Discard, rdr) + if err != nil { + t.Fatalf("WriteTo/Copy: %v", err) + } + if n != 6 { + t.Fatalf("copied %d, want 6", n) + } + + done := make(chan struct{}) + var rn int + var rerr error + go func() { + defer close(done) + var buf [8]byte + rn, rerr = rdr.Read(buf[:]) + }() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("Read after WriteTo deadlocked") + } + if rn != 0 || rerr != io.EOF { + t.Fatalf("Read after drain: n=%d err=%v, want 0, EOF", rn, rerr) + } +} + +func TestWriteToDoesNotReturnEOF(t *testing.T) { + gzipData := []byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0x48, + 0xcd, 0xc9, 0xc9, 0xe7, 0x02, 0x00, 0x20, 0x30, 0x3a, 0x36, 0x06, 0x00, + 0x00, 0x00, + } + rdr, err := NewReader(bytes.NewReader(gzipData)) + if err != nil { + t.Fatal(err) + } + defer rdr.Close() + n, err := rdr.WriteTo(io.Discard) + if err != nil { + t.Fatalf("WriteTo err=%v, want nil (not EOF)", err) + } + if n != 6 { + t.Fatalf("n=%d want 6", n) + } +} From 57d6b2926129fa95429ed47311b8423b6781b5f6 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 02:01:30 +0800 Subject: [PATCH 2/3] fix: clear lastBlock/current on Reset after WriteTo drain --- gunzip.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gunzip.go b/gunzip.go index 6e365eb..87365d7 100644 --- a/gunzip.go +++ b/gunzip.go @@ -172,6 +172,9 @@ func (z *Reader) Reset(r io.Reader) error { z.digest = crc32.NewIEEE() z.size = 0 z.err = nil + z.lastBlock = false + z.current = nil + z.roff = 0 z.multistream.Store(true) z.readAheadStarted.Store(false) From 22ce4dd3b2eec362ccab547c9cdd7356cea9a32a Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 23:53:14 +0800 Subject: [PATCH 3/3] fix: return WriteTo buffers on writer errors --- gunzip.go | 6 ++++++ gunzip_test.go | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gunzip.go b/gunzip.go index 87365d7..89400b9 100644 --- a/gunzip.go +++ b/gunzip.go @@ -571,10 +571,16 @@ func (z *Reader) WriteTo(w io.Writer) (n int64, err error) { if len(read.b) > 0 { n, err := w.Write(read.b) if n != len(read.b) { + if cap(read.b) > 0 { + z.blockPool <- read.b + } return total, io.ErrShortWrite } total += int64(n) if err != nil { + if cap(read.b) > 0 { + z.blockPool <- read.b + } return total, err } } diff --git a/gunzip_test.go b/gunzip_test.go index e81618f..32eb936 100644 --- a/gunzip_test.go +++ b/gunzip_test.go @@ -848,7 +848,6 @@ func TestWriterTo(t *testing.T) { }) } - func TestReadAfterWriteToNoDeadlock(t *testing.T) { // echo hello | gzip -c gzipData := []byte{