From da723e15d7492143459c65e4fecf2c17dc0fa1e4 Mon Sep 17 00:00:00 2001 From: Daniel Morsing Date: Sun, 25 Jan 2026 14:42:26 +0000 Subject: crypto/tls: avoid atLeastReader and associated allocations. Go 1.21 introduced bytes.Buffer.AvailableBuffer. This mechanism lets us write directly into a Buffer's backing memory while also implementing custom logic without going through interfaces. atLeastReader caused an allocation on every read. On workloads with very small reads, these allocations can add up to substantial overhead. By using Buffer.AvailableBytes(), we can avoid these allocations and an interface indirection in the Read fast path. Fixes #58249. Change-Id: Icf26ec1dd7ef88154c47356ef9c26a516a6a6964 Reviewed-on: https://go-review.googlesource.com/c/go/+/739980 Reviewed-by: David Chase LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao Reviewed-by: Daniel McCarney --- src/crypto/tls/conn.go | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/crypto/tls/conn.go b/src/crypto/tls/conn.go index 9c662ef8f6..54a4d1a883 100644 --- a/src/crypto/tls/conn.go +++ b/src/crypto/tls/conn.go @@ -800,29 +800,6 @@ func (c *Conn) retryReadRecord(expectChangeCipherSpec bool) error { return c.readRecordOrCCS(expectChangeCipherSpec) } -// atLeastReader reads from R, stopping with EOF once at least N bytes have been -// read. It is different from an io.LimitedReader in that it doesn't cut short -// the last Read call, and in that it considers an early EOF an error. -type atLeastReader struct { - R io.Reader - N int64 -} - -func (r *atLeastReader) Read(p []byte) (int, error) { - if r.N <= 0 { - return 0, io.EOF - } - n, err := r.R.Read(p) - r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809 - if r.N > 0 && err == io.EOF { - return n, io.ErrUnexpectedEOF - } - if r.N <= 0 && err == nil { - return n, io.EOF - } - return n, err -} - // readFromUntil reads from r into c.rawInput until c.rawInput contains // at least n bytes or else returns an error. func (c *Conn) readFromUntil(r io.Reader, n int) error { @@ -833,9 +810,31 @@ func (c *Conn) readFromUntil(r io.Reader, n int) error { // There might be extra input waiting on the wire. Make a best effort // attempt to fetch it so that it can be used in (*Conn).Read to // "predict" closeNotify alerts. + // TODO(dmo): we use bytes.MinRead here because we used the buffer + // ReadFrom mechanism to avoid allocations, but we've hoisted this + // loop for performance. We really should use our own heuristic here + // for how much to read ahead. c.rawInput.Grow(needs + bytes.MinRead) - _, err := c.rawInput.ReadFrom(&atLeastReader{r, int64(needs)}) - return err + for { + buf := c.rawInput.AvailableBuffer()[:needs+bytes.MinRead] + n, err := r.Read(buf) + // This write is just to update the internal state of the + // rawInput bytes.Buffer. It cannot fail. + c.rawInput.Write(buf[:n]) + needs -= n + if needs <= 0 { + if err == io.EOF { + err = nil + } + return err + } + if err == io.EOF { + return io.ErrUnexpectedEOF + } + if err != nil { + return err + } + } } // sendAlertLocked sends a TLS alert message. -- cgit v1.3