Repository: cloudwego/hertz
Pull request #698 (2023-03-31 10:47:36)

fix: standard conn may panic after close
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
fix
#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
- [ ] The description of this PR title is user-oriented and clear enough for others to understand.


#### (Optional) Translate the PR title into Chinese.
修复标准库 Conn 在关闭之后对其进行读写操作会 panic 的问题

#### (Optional) More detail description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review. If it is a perf type PR, perf data is suggested to give.
-->
en: When using the standard library's Conn concurrently, if you read or write to the Conn after closing it, you will get a panic
zh(optional): 在并发使用标准库的 Conn 时，如果在关闭了 Conn 之后再读写 Conn，会出现 panic

#### Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->


codecov[bot]: ## [Codecov](https://codecov.io/gh/cloudwego/hertz/pull/698?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Patch coverage: **`100.00`**% and project coverage change: **`+0.03`** :tada:
> Comparison is base [(`c98aec7`)](https://codecov.io/gh/cloudwego/hertz/commit/c98aec73913a08211bdc6e4c2a373523f7264e7a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 72.49% compared to head [(`74bce15`)](https://codecov.io/gh/cloudwego/hertz/pull/698?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) 72.53%.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop     #698      +/-   ##
===========================================
+ Coverage    72.49%   72.53%   +0.03%     
===========================================
  Files           96       96              
  Lines         9392     9393       +1     
===========================================
+ Hits          6809     6813       +4     
+ Misses        2154     2151       -3     
  Partials       429      429              
```


| [Impacted Files](https://codecov.io/gh/cloudwego/hertz/pull/698?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Coverage Δ | |
|---|---|---|
| [pkg/network/standard/connection.go](https://codecov.io/gh/cloudwego/hertz/pull/698?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL25ldHdvcmsvc3RhbmRhcmQvY29ubmVjdGlvbi5nbw==) | `80.12% <100.00%> (+0.06%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://codecov.io/gh/cloudwego/hertz/pull/698/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/cloudwego/hertz/pull/698?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).


Patch:
diff --git a/go.mod b/go.mod
index 2c7a975..79b5fab 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.16
 require (
 	github.com/bytedance/go-tagexpr/v2 v2.9.2
 	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7
+	github.com/bytedance/mockey v1.2.1
 	github.com/bytedance/sonic v1.8.1
 	github.com/cloudwego/netpoll v0.3.1
 	github.com/fsnotify/fsnotify v1.5.4
diff --git a/go.sum b/go.sum
index fe7ecbf..f9ea5da 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6H
 github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM=
 github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
+github.com/bytedance/mockey v1.2.1 h1:g84ngI88hz1DR4wZTL3yOuqlEcq67MretBfQUdXwrmw=
+github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
 github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -20,17 +22,25 @@ github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
 github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk=
 github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
 github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0=
 github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
 github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -50,13 +60,19 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
diff --git a/pkg/network/standard/connection.go b/pkg/network/standard/connection.go
index 5ab9a8c..65fd08f 100644
--- a/pkg/network/standard/connection.go
+++ b/pkg/network/standard/connection.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"io"
 	"net"
+	"runtime"
 	"strconv"
 	"syscall"
 	"time"
@@ -174,9 +175,6 @@ func (c *Conn) ReadFrom(r io.Reader) (n int64, err error) {
 
 // Close closes the connection
 func (c *Conn) Close() error {
-	c.inputBuffer.release()
-	c.outputBuffer.release()
-	c.inputBuffer, c.outputBuffer = nil, nil
 	return c.c.Close()
 }
 
@@ -548,20 +546,27 @@ func newConn(c net.Conn, size int) network.Conn {
 	if size > maxSize {
 		maxSize = size
 	}
-	inputNode := newBufferNode(maxSize)
+
+	node := newBufferNode(maxSize)
+	inputBuffer := &linkBuffer{
+		head:  node,
+		read:  node,
+		write: node,
+	}
+	runtime.SetFinalizer(inputBuffer, (*linkBuffer).release)
+
 	outputNode := newBufferNode(0)
+	outputBuffer := &linkBuffer{
+		head:  outputNode,
+		write: outputNode,
+	}
+	runtime.SetFinalizer(outputBuffer, (*linkBuffer).release)
+
 	return &Conn{
-		c: c,
-		inputBuffer: &linkBuffer{
-			head:  inputNode,
-			read:  inputNode,
-			write: inputNode,
-		},
-		outputBuffer: &linkBuffer{
-			head:  outputNode,
-			write: outputNode,
-		},
-		maxSize: maxSize,
+		c:            c,
+		inputBuffer:  inputBuffer,
+		outputBuffer: outputBuffer,
+		maxSize:      maxSize,
 	}
 }
 
@@ -570,21 +575,28 @@ func newTLSConn(c net.Conn, size int) network.Conn {
 	if size > maxSize {
 		maxSize = size
 	}
+
 	node := newBufferNode(maxSize)
+	inputBuffer := &linkBuffer{
+		head:  node,
+		read:  node,
+		write: node,
+	}
+	runtime.SetFinalizer(inputBuffer, (*linkBuffer).release)
+
 	outputNode := newBufferNode(0)
+	outputBuffer := &linkBuffer{
+		head:  outputNode,
+		write: outputNode,
+	}
+	runtime.SetFinalizer(outputBuffer, (*linkBuffer).release)
+
 	return &TLSConn{
 		Conn{
-			c: c,
-			inputBuffer: &linkBuffer{
-				head:  node,
-				read:  node,
-				write: node,
-			},
-			outputBuffer: &linkBuffer{
-				head:  outputNode,
-				write: outputNode,
-			},
-			maxSize: maxSize,
+			c:            c,
+			inputBuffer:  inputBuffer,
+			outputBuffer: outputBuffer,
+			maxSize:      maxSize,
 		},
 	}
 }
diff --git a/pkg/protocol/http1/client.go b/pkg/protocol/http1/client.go
index 8a35aed..353ecc8 100644
--- a/pkg/protocol/http1/client.go
+++ b/pkg/protocol/http1/client.go
@@ -619,11 +619,21 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 	}
 	zr := c.acquireReader(conn)
 
+	// init here for passing in ReadBodyStream's closure
+	// and this value will be assigned after reading Response's Header
+	//
+	// This is to solve the circular dependency problem of Response and BodyStream
+	shouldCloseConn := false
+
 	if !c.ResponseBodyStream {
 		err = respI.ReadHeaderAndLimitBody(resp, zr, c.MaxResponseBodySize)
 	} else {
 		err = respI.ReadBodyStream(resp, zr, c.MaxResponseBodySize, func() error {
-			c.releaseConn(cc)
+			if shouldCloseConn {
+				c.closeConn(cc)
+			} else {
+				c.releaseConn(cc)
+			}
 			return nil
 		})
 	}
@@ -638,11 +648,13 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
 
 	zr.Release() //nolint:errcheck
 
+	shouldCloseConn = resetConnection || req.ConnectionClose() || resp.ConnectionClose()
+
 	if c.ResponseBodyStream {
 		return false, err
 	}
 
-	if resetConnection || req.ConnectionClose() || resp.ConnectionClose() {
+	if shouldCloseConn {
 		c.closeConn(cc)
 	} else {
 		c.releaseConn(cc)


Test patch:
diff --git a/pkg/app/client/client_test.go b/pkg/app/client/client_test.go
index b6eb263..8862d84 100644
--- a/pkg/app/client/client_test.go
+++ b/pkg/app/client/client_test.go
@@ -1951,6 +1951,58 @@ func TestClientReadResponseBodyStreamWithDoubleRequest(t *testing.T) {
 	}
 }
 
+func TestClientReadResponseBodyStreamWithConnectionClose(t *testing.T) {
+	part1 := ""
+	for i := 0; i < 8192; i++ {
+		part1 += "a"
+	}
+
+	opt := config.NewOptions([]config.Option{})
+	opt.Addr = "127.0.0.1:10036"
+	engine := route.NewEngine(opt)
+	engine.POST("/", func(ctx context.Context, c *app.RequestContext) {
+		c.String(consts.StatusOK, part1)
+	})
+	go engine.Run()
+	time.Sleep(100 * time.Millisecond)
+
+	client, _ := NewClient(WithResponseBodyStream(true))
+
+	// first req
+	req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+	defer func() {
+		protocol.ReleaseRequest(req)
+		protocol.ReleaseResponse(resp)
+	}()
+	req.SetConnectionClose()
+	req.SetMethod(consts.MethodPost)
+	req.SetRequestURI("http://127.0.0.1:10036")
+
+	err := client.Do(context.Background(), req, resp)
+	if err != nil {
+		t.Fatalf("client Do error=%v", err.Error())
+	}
+
+	assert.DeepEqual(t, part1, string(resp.Body()))
+
+	// second req
+	req1, resp1 := protocol.AcquireRequest(), protocol.AcquireResponse()
+	defer func() {
+		protocol.ReleaseRequest(req1)
+		protocol.ReleaseResponse(resp1)
+	}()
+	req1.SetConnectionClose()
+	req1.SetMethod(consts.MethodPost)
+	req1.SetRequestURI("http://127.0.0.1:10036")
+
+	err = client.Do(context.Background(), req1, resp1)
+	if err != nil {
+		t.Fatalf("client Do error=%v", err.Error())
+	}
+
+	assert.DeepEqual(t, part1, string(resp1.Body()))
+}
+
 type mockDialer struct {
 	network.Dialer
 	customDialerFunc func(network, address string, timeout time.Duration, tlsConfig *tls.Config)
diff --git a/pkg/common/bytebufferpool/pool_test.go b/pkg/common/bytebufferpool/pool_test.go
index 1c2bc7e..0a26197 100644
--- a/pkg/common/bytebufferpool/pool_test.go
+++ b/pkg/common/bytebufferpool/pool_test.go
@@ -97,7 +97,7 @@ func TestPoolVariousSizesConcurrent(t *testing.T) {
 	for i := 0; i < concurrency; i++ {
 		select {
 		case <-ch:
-		case <-time.After(3 * time.Second):
+		case <-time.After(10 * time.Second):
 			t.Fatalf("timeout")
 		}
 	}
diff --git a/pkg/network/standard/connection_test.go b/pkg/network/standard/connection_test.go
index 9b0fafd..6fcc456 100644
--- a/pkg/network/standard/connection_test.go
+++ b/pkg/network/standard/connection_test.go
@@ -22,11 +22,14 @@ import (
 	"errors"
 	"io"
 	"net"
+	"runtime"
 	"strings"
+	"sync/atomic"
 	"syscall"
 	"testing"
 	"time"
 
+	. "github.com/bytedance/mockey"
 	"github.com/cloudwego/hertz/pkg/common/test/assert"
 )
 
@@ -145,11 +148,11 @@ func TestPeekRelease(t *testing.T) {
 	}
 
 	// test cross node
-	b, _ = conn.Peek(100000000)
-	if len(b) != 100000000 {
+	b, _ = conn.Peek(1000000)
+	if len(b) != 1000000 {
 		t.Errorf("unexpected len(b): %v, expected 1", len(b))
 	}
-	conn.Skip(100000000)
+	conn.Skip(1000000)
 	conn.Release()
 
 	// test maxSize
@@ -184,7 +187,7 @@ func TestReadBytes(t *testing.T) {
 	}
 	bbb, _ := conn.ReadByte()
 	if bbb != 0 {
-		t.Errorf("unexpected bbb: %v, expected nil", bbb)
+		t.Errorf("unexpected bbb: %v, expected nil", string(bbb))
 	}
 	if conn.Len() != 4094 {
 		t.Errorf("unexpected conn.Len: %v, expected 4094", conn.Len())
@@ -286,6 +289,9 @@ func (m *mockConn) ConnectionState() tls.ConnectionState {
 
 func (m mockConn) Read(b []byte) (n int, err error) {
 	length := len(b)
+	for i := 0; i < length; i++ {
+		b[i] = 0
+	}
 	if length > 8192 {
 		return 8192, nil
 	}
@@ -342,3 +348,33 @@ func (m *mockAddr) Network() string {
 func (m *mockAddr) String() string {
 	return m.address
 }
+
+var release_count uint32 = 0
+
+func mockLinkBufferNodeRelease(b *linkBufferNode) {
+	atomic.AddUint32(&release_count, 1)
+
+	if !b.readOnly {
+		free(b.buf)
+	}
+	b.readOnly = false
+	b.buf = nil
+	b.next = nil
+	b.malloc, b.off = 0, 0
+	bufferPool.Put(b)
+}
+
+func TestConnSetFinalizer(t *testing.T) {
+	runtime.GC()
+	time.Sleep(time.Millisecond * 100)
+
+	Mock((*linkBufferNode).Release).To(mockLinkBufferNodeRelease).Build()
+
+	atomic.StoreUint32(&release_count, 0)
+	_ = newConn(&mockConn{}, 4096)
+
+	runtime.GC()
+	time.Sleep(time.Millisecond * 100)
+
+	assert.DeepEqual(t, uint32(2), atomic.LoadUint32(&release_count))
+}