From b09f27a86140ff66743cb60bef693d5c4086d931 Mon Sep 17 00:00:00 2001 From: Dwi Siswanto Date: Sat, 29 Nov 2025 01:47:04 +0700 Subject: [PATCH 1/2] test(httputil): add TestResponseChain_StringSafety Signed-off-by: Dwi Siswanto --- http/respChain_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/http/respChain_test.go b/http/respChain_test.go index fdfe001..777830d 100644 --- a/http/respChain_test.go +++ b/http/respChain_test.go @@ -1182,3 +1182,48 @@ func TestLimitedBuffer_Pool(t *testing.T) { require.Equal(t, len(data), buf.Len()) require.Equal(t, data, buf.Bytes()) } + +func TestResponseChain_StringSafety(t *testing.T) { + bodyContent := "Original Body Content That Should Be Preserved Yeah Okay LOL" + headerKey := "X-Safety-Test" + headerValue := "Original Header Value" + + resp := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(bodyContent)), + Header: http.Header{headerKey: []string{headerValue}}, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + } + + rc := NewResponseChain(resp, -1) + err := rc.Fill() + require.NoError(t, err) + + bodyStr := rc.BodyString() + headersStr := rc.HeadersString() + + assert.Equal(t, bodyContent, bodyStr) + assert.Contains(t, headersStr, headerValue) + + rc.Close() + + // Now attempt to pollute the pool and overwrite the memory. + // We get a bunch of buffers and write garbage to them. + var buffers []*bytes.Buffer + for i := 0; i < 100; i++ { + buf := getBuffer() + buffers = append(buffers, buf) + + buf.Reset() + buf.WriteString("ALERTA_GARBAGE_DATA_OVERWRITING_MEMORY_ALERTA_GARBAGE_DATA_OVERWRITING_MEMORY") + } + + for _, buf := range buffers { + putBuffer(buf) + } + + assert.Equal(t, bodyContent, bodyStr, "BodyString() content changed after buffer reuse - unsafe memory sharing detected") + assert.Contains(t, headersStr, headerValue, "HeadersString() content changed after buffer reuse - unsafe memory sharing detected") +} From 99ca5c15421bec350807ccca571d4d4be484b38c Mon Sep 17 00:00:00 2001 From: Dwi Siswanto Date: Sat, 29 Nov 2025 13:06:41 +0700 Subject: [PATCH 2/2] fix(httputil): prevent memory corruption in `ResponseChain` string accessors. Replaces unsafe zero-copy `conversion.String()` with `bytes.Buffer.String()` in `HeadersString()` and `BodyString()` methods. The previous (>= v0.7.0) implementation used `conversion.String()` on the buffer's byte slice, which created a string that shared memory with the pooled `bytes.Buffer`. When buffers were returned to the pool (via `Close()`) and reused, the strings held by consumers would be corrupted lmao. Signed-off-by: Dwi Siswanto --- http/respChain.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/http/respChain.go b/http/respChain.go index 4b07b0d..63cff9e 100644 --- a/http/respChain.go +++ b/http/respChain.go @@ -259,10 +259,9 @@ func (r *ResponseChain) HeadersBytes() []byte { // HeadersString returns the current response headers as string in the chain. // -// The returned string is valid only until Close() is called. -// This is a zero-copy operation for performance. +// The returned string is a copy and remains valid even after Close() is called. func (r *ResponseChain) HeadersString() string { - return conversion.String(r.headers.Bytes()) + return r.headers.String() } // Body returns the current response body buffer in the chain. @@ -282,10 +281,9 @@ func (r *ResponseChain) BodyBytes() []byte { // BodyString returns the current response body as string in the chain. // -// The returned string is valid only until Close() is called. -// This is a zero-copy operation for performance. +// The returned string is a copy and remains valid even after Close() is called. func (r *ResponseChain) BodyString() string { - return conversion.String(r.body.Bytes()) + return r.body.String() } // FullResponse returns a new buffer containing headers+body. @@ -319,7 +317,6 @@ func (r *ResponseChain) FullResponseBytes() []byte { // FullResponseString returns the current response as string in the chain. // // The returned string is a copy and remains valid even after Close() is called. -// This is a zero-copy operation from the byte slice. func (r *ResponseChain) FullResponseString() string { return conversion.String(r.FullResponseBytes()) }