[release-branch.go1.19] mime/multipart: avoid excessive copy buffer allocations in ReadForm

When copying form data to disk with io.Copy,
allocate only one copy buffer and reuse it rather than
creating two buffers per file (one from io.multiReader.WriteTo,
and a second one from os.File.ReadFrom).

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

For CVE-2023-24536
For #59153
For #59269

Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802453
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802395
Run-TryBot: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Change-Id: Ie405470c92abffed3356913b37d813e982c96c8b
Reviewed-on: https://go-review.googlesource.com/c/go/+/481983
Run-TryBot: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
This commit is contained in:
Damien Neil 2023-03-16 14:18:04 -07:00 committed by Gopher Robot
parent d6759e7a05
commit ef41a4e2fa
2 changed files with 61 additions and 3 deletions

View File

@ -84,6 +84,7 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
maxMemoryBytes = math.MaxInt64
}
}
var copyBuf []byte
for {
p, err := r.nextPart(false, maxMemoryBytes)
if err == io.EOF {
@ -147,14 +148,22 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
}
}
numDiskFiles++
size, err := io.Copy(file, io.MultiReader(&b, p))
if _, err := file.Write(b.Bytes()); err != nil {
return nil, err
}
if copyBuf == nil {
copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses
}
// os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it.
type writerOnly struct{ io.Writer }
remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf)
if err != nil {
return nil, err
}
fh.tmpfile = file.Name()
fh.Size = size
fh.Size = int64(b.Len()) + remainingSize
fh.tmpoff = fileOff
fileOff += size
fileOff += fh.Size
if !combineFiles {
if err := file.Close(); err != nil {
return nil, err

View File

@ -368,3 +368,52 @@ func testReadFormManyFiles(t *testing.T, distinct bool) {
t.Fatalf("temp dir contains %v files; want 0", len(names))
}
}
func BenchmarkReadForm(b *testing.B) {
for _, test := range []struct {
name string
form func(fw *Writer, count int)
}{{
name: "fields",
form: func(fw *Writer, count int) {
for i := 0; i < count; i++ {
w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i))
fmt.Fprintf(w, "value %v", i)
}
},
}, {
name: "files",
form: func(fw *Writer, count int) {
for i := 0; i < count; i++ {
w, _ := fw.CreateFormFile(fmt.Sprintf("field%v", i), fmt.Sprintf("file%v", i))
fmt.Fprintf(w, "value %v", i)
}
},
}} {
b.Run(test.name, func(b *testing.B) {
for _, maxMemory := range []int64{
0,
1 << 20,
} {
var buf bytes.Buffer
fw := NewWriter(&buf)
test.form(fw, 10)
if err := fw.Close(); err != nil {
b.Fatal(err)
}
b.Run(fmt.Sprintf("maxMemory=%v", maxMemory), func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary())
form, err := fr.ReadForm(maxMemory)
if err != nil {
b.Fatal(err)
}
form.RemoveAll()
}
})
}
})
}
}