aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Morsing <daniel.morsing@gmail.com>2026-01-25 14:42:26 +0000
committerDaniel Morsing <daniel.morsing@gmail.com>2026-04-07 10:40:43 -0700
commitda723e15d7492143459c65e4fecf2c17dc0fa1e4 (patch)
treef17d3f17c506705a3a0718b4e00fa5915f8be711
parent7394184e4ecf7cd1e938b80434f057caeb5ffa58 (diff)
downloadgo-da723e15d7492143459c65e4fecf2c17dc0fa1e4.tar.xz
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 <drchase@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Junyang Shao <shaojunyang@google.com> Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
-rw-r--r--src/crypto/tls/conn.go49
1 files 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.