Skip to content

Commit

Permalink
[release-branch.go1.20] net/textproto: avoid overpredicting the numbe…
Browse files Browse the repository at this point in the history
…r of MIME header keys

A parsed MIME header is a map[string][]string. In the common case,
a header contains many one-element []string slices. To avoid
allocating a separate slice for each key, ReadMIMEHeader looks
ahead in the input to predict the number of keys that will be
parsed, and allocates a single []string of that length.
The individual slices are then allocated out of the larger one.

The prediction of the number of header keys was done by counting
newlines in the input buffer, which does not take into account
header continuation lines (where a header key/value spans multiple
lines) or the end of the header block and the start of the body.
This could lead to a substantial amount of overallocation, for
example when the body consists of nothing but a large block of
newlines.

Fix header key count prediction to take into account the end of
the headers (indicated by a blank line) and continuation lines
(starting with whitespace).

Thanks to Jakob Ackermann (@das7pad) for reporting this issue.

Fixes CVE-2023-24534
For #58975
Fixes #59268

Change-Id: I0591593e67b6fdba22a32dcc3334fad797727f5c
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802452
Run-TryBot: Damien Neil <[email protected]>
Reviewed-by: Roland Shoemaker <[email protected]>
Reviewed-by: Julie Qiu <[email protected]>
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802397
Run-TryBot: Roland Shoemaker <[email protected]>
TryBot-Result: Security TryBots <[email protected]>
Reviewed-on: https://go-review.googlesource.com/c/go/+/481988
Run-TryBot: Michael Knyszek <[email protected]>
TryBot-Bypass: Michael Knyszek <[email protected]>
Auto-Submit: Michael Knyszek <[email protected]>
Reviewed-by: Matthew Dempsky <[email protected]>
  • Loading branch information
neild authored and gopherbot committed Apr 4, 2023
1 parent 9a164d1 commit 3991f6c
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 4 deletions.
24 changes: 20 additions & 4 deletions src/net/textproto/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,8 +489,11 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
// large one ahead of time which we'll cut up into smaller
// slices. If this isn't big enough later, we allocate small ones.
var strs []string
hint := r.upcomingHeaderNewlines()
hint := r.upcomingHeaderKeys()
if hint > 0 {
if hint > 1000 {
hint = 1000 // set a cap to avoid overallocation
}
strs = make([]string, hint)
}

Expand Down Expand Up @@ -581,17 +584,30 @@ func mustHaveFieldNameColon(line []byte) error {

var nl = []byte("\n")

// upcomingHeaderNewlines returns an approximation of the number of newlines
// upcomingHeaderKeys returns an approximation of the number of keys
// that will be in this header. If it gets confused, it returns 0.
func (r *Reader) upcomingHeaderNewlines() (n int) {
func (r *Reader) upcomingHeaderKeys() (n int) {
// Try to determine the 'hint' size.
r.R.Peek(1) // force a buffer load if empty
s := r.R.Buffered()
if s == 0 {
return
}
peek, _ := r.R.Peek(s)
return bytes.Count(peek, nl)
for len(peek) > 0 && n < 1000 {
var line []byte
line, peek, _ = bytes.Cut(peek, nl)
if len(line) == 0 || (len(line) == 1 && line[0] == '\r') {
// Blank line separating headers from the body.
break
}
if line[0] == ' ' || line[0] == '\t' {
// Folded continuation of the previous line.
continue
}
n++
}
return n
}

// CanonicalMIMEHeaderKey returns the canonical format of the
Expand Down
59 changes: 59 additions & 0 deletions src/net/textproto/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net"
"reflect"
"runtime"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -129,6 +130,42 @@ func TestReadMIMEHeaderSingle(t *testing.T) {
}
}

// TestReaderUpcomingHeaderKeys is testing an internal function, but it's very
// difficult to test well via the external API.
func TestReaderUpcomingHeaderKeys(t *testing.T) {
for _, test := range []struct {
input string
want int
}{{
input: "",
want: 0,
}, {
input: "A: v",
want: 1,
}, {
input: "A: v\r\nB: v\r\n",
want: 2,
}, {
input: "A: v\nB: v\n",
want: 2,
}, {
input: "A: v\r\n continued\r\n still continued\r\nB: v\r\n\r\n",
want: 2,
}, {
input: "A: v\r\n\r\nB: v\r\nC: v\r\n",
want: 1,
}, {
input: "A: v" + strings.Repeat("\n", 1000),
want: 1,
}} {
r := reader(test.input)
got := r.upcomingHeaderKeys()
if test.want != got {
t.Fatalf("upcomingHeaderKeys(%q): %v; want %v", test.input, got, test.want)
}
}
}

func TestReadMIMEHeaderNoKey(t *testing.T) {
r := reader(": bar\ntest-1: 1\n\n")
m, err := r.ReadMIMEHeader()
Expand Down Expand Up @@ -271,6 +308,28 @@ func TestReadMIMEHeaderTrimContinued(t *testing.T) {
}
}

// Test that reading a header doesn't overallocate. Issue 58975.
func TestReadMIMEHeaderAllocations(t *testing.T) {
var totalAlloc uint64
const count = 200
for i := 0; i < count; i++ {
r := reader("A: b\r\n\r\n" + strings.Repeat("\n", 4096))
var m1, m2 runtime.MemStats
runtime.ReadMemStats(&m1)
_, err := r.ReadMIMEHeader()
if err != nil {
t.Fatalf("ReadMIMEHeader: %v", err)
}
runtime.ReadMemStats(&m2)
totalAlloc += m2.TotalAlloc - m1.TotalAlloc
}
// 32k is large and we actually allocate substantially less,
// but prior to the fix for #58975 we allocated ~400k in this case.
if got, want := totalAlloc/count, uint64(32768); got > want {
t.Fatalf("ReadMIMEHeader allocated %v bytes, want < %v", got, want)
}
}

type readResponseTest struct {
in string
inCode int
Expand Down

0 comments on commit 3991f6c

Please sign in to comment.