From 02662dd122b49b925111490ed17bc8fc1b7ac82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?= Date: Mon, 14 Sep 2026 15:11:08 +0300 Subject: [PATCH] fix ConnReadN leaking a read goroutine when stop races cancellation --- reader/conn_read.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/reader/conn_read.go b/reader/conn_read.go index af0570f..25b3712 100644 --- a/reader/conn_read.go +++ b/reader/conn_read.go @@ -62,7 +62,16 @@ func ConnReadN(ctx context.Context, reader io.Reader, N int64) ([]byte, error) { // connection. The deferred stop cancels this on the normal path, leaving // the deadline untouched when the read finishes in time. if rd, ok := reader.(interface{ SetReadDeadline(time.Time) error }); ok { - defer context.AfterFunc(ctx, func() { _ = rd.SetReadDeadline(time.Now()) })() + expire := func() { _ = rd.SetReadDeadline(time.Now()) } + stop := context.AfterFunc(ctx, expire) + defer func() { + // A parent cancellation closes Done before it runs AfterFunc + // callbacks, so stop can win after we observed Done and skip + // expire, leaving the read blocked forever. + if stop() && readErr != nil && ctx.Err() != nil { + expire() + } + }() } _, readErr = contextutil.ExecFuncWithTwoReturns(ctx, fn) // On cancellation report the context error rather than the net timeout