From a599cac78c9927db9cadcef52151e8888e202e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Thu, 15 Oct 2026 11:49:17 +0300 Subject: [PATCH 1/2] fix: copy byte slices given to NewReusableReadCloser --- reader/reusable_read_closer.go | 8 ++++---- reader/reusable_read_closer_test.go | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/reader/reusable_read_closer.go b/reader/reusable_read_closer.go index b33579d0..9ec745d8 100644 --- a/reader/reusable_read_closer.go +++ b/reader/reusable_read_closer.go @@ -25,12 +25,12 @@ func NewReusableReadCloser(raw interface{}) (*ReusableReadCloser, error) { switch body := raw.(type) { case []byte: - // if a byte array , create buffer from bytes and use it - readBuf = *bytes.NewBuffer(body) + // copy: reset writes the buffer back, which must not reach the + // caller's slice or another reader built from it + readBuf = *bytes.NewBuffer(bytes.Clone(body)) case *[]byte: - // if *[]byte, create buffer from bytes and use it - readBuf = *bytes.NewBuffer(*body) + readBuf = *bytes.NewBuffer(bytes.Clone(*body)) case string: // if a string , create buffer from string and use it diff --git a/reader/reusable_read_closer_test.go b/reader/reusable_read_closer_test.go index d5c75e50..23039dfd 100644 --- a/reader/reusable_read_closer_test.go +++ b/reader/reusable_read_closer_test.go @@ -35,7 +35,7 @@ func TestReusableReader(t *testing.T) { } }) - // todo: readers shouldn't be used concurrently, so here we just try to catch pontential read concurring with resets panics + // todo: readers shouldn't be used concurrently, so here we just try to catch potential read concurring with resets panics t.Run("concurrent-reset-with-read", func(t *testing.T) { reusableReader, err := NewReusableReadCloser(v) require.Nil(t, err) @@ -58,3 +58,24 @@ func TestReusableReader(t *testing.T) { }) } } + +// Readers must own their bytes: one built from a slice the caller reuses, or +// several built from one slice and read concurrently, would otherwise see each +// other's writes, since a reader writes its buffer back when it hits EOF. +func TestReusableReaderCopiesByteSlices(t *testing.T) { + for name, build := range map[string]func([]byte) (*ReusableReadCloser, error){ + "[]byte": func(b []byte) (*ReusableReadCloser, error) { return NewReusableReadCloser(b) }, + "*[]byte": func(b []byte) (*ReusableReadCloser, error) { return NewReusableReadCloser(&b) }, + } { + t.Run(name, func(t *testing.T) { + body := []byte("original") + reader, err := build(body) + require.NoError(t, err) + copy(body, "modified") + + got, err := io.ReadAll(reader) + require.NoError(t, err) + require.Equal(t, "original", string(got)) + }) + } +} From 10aed69174d6c59cf6a05d5cce79a9277b09631b Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Sun, 18 Oct 2026 15:52:10 +0200 Subject: [PATCH 2/2] test readers --- reader/reusable_read_closer_test.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/reader/reusable_read_closer_test.go b/reader/reusable_read_closer_test.go index 23039dfd..a55ae351 100644 --- a/reader/reusable_read_closer_test.go +++ b/reader/reusable_read_closer_test.go @@ -59,9 +59,9 @@ func TestReusableReader(t *testing.T) { } } -// Readers must own their bytes: one built from a slice the caller reuses, or -// several built from one slice and read concurrently, would otherwise see each -// other's writes, since a reader writes its buffer back when it hits EOF. +// Readers must own their bytes. reset writes the buffer back at EOF, so a +// caller that reuses the slice, and a second reader built from it, must still +// see the original body. func TestReusableReaderCopiesByteSlices(t *testing.T) { for name, build := range map[string]func([]byte) (*ReusableReadCloser, error){ "[]byte": func(b []byte) (*ReusableReadCloser, error) { return NewReusableReadCloser(b) }, @@ -76,6 +76,23 @@ func TestReusableReaderCopiesByteSlices(t *testing.T) { got, err := io.ReadAll(reader) require.NoError(t, err) require.Equal(t, "original", string(got)) + require.Equal(t, "modified", string(body)) + }) + + t.Run(name+" two readers", func(t *testing.T) { + body := []byte("original") + first, err := build(body) + require.NoError(t, err) + second, err := build(body) + require.NoError(t, err) + copy(body, "modified") + + for _, reader := range []*ReusableReadCloser{first, second, first} { + got, err := io.ReadAll(reader) + require.NoError(t, err) + require.Equal(t, "original", string(got)) + } + require.Equal(t, "modified", string(body)) }) } }