Skip to content

Commit

Permalink
[release-branch.go1.20] mime/multipart: avoid excessive copy buffer a…
Browse files Browse the repository at this point in the history
…llocations 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 #59270

Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802453
Run-TryBot: Damien Neil <[email protected]>
Reviewed-by: Julie Qiu <[email protected]>
Reviewed-by: Roland Shoemaker <[email protected]>
Change-Id: I44ef17c4b4964cdac2858317275594194801fee3
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802398
Run-TryBot: Roland Shoemaker <[email protected]>
Reviewed-on: https://go-review.googlesource.com/c/go/+/481989
Auto-Submit: Michael Knyszek <[email protected]>
Run-TryBot: Michael Knyszek <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
Reviewed-by: Matthew Dempsky <[email protected]>
  • Loading branch information
neild authored and gopherbot committed Apr 4, 2023
1 parent 3991f6c commit ea6b5a6
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 3 deletions.
15 changes: 12 additions & 3 deletions src/mime/multipart/formdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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 {
Expand Down Expand Up @@ -148,14 +149,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
Expand Down
49 changes: 49 additions & 0 deletions src/mime/multipart/formdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

})
}
})
}
}

0 comments on commit ea6b5a6

Please sign in to comment.