Skip to content

Commit ef41a4e

Browse files
neildgopherbot
authored andcommitted
[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 <[email protected]> Reviewed-by: Julie Qiu <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802395 Run-TryBot: Roland Shoemaker <[email protected]> Reviewed-by: Damien Neil <[email protected]> Change-Id: Ie405470c92abffed3356913b37d813e982c96c8b Reviewed-on: https://go-review.googlesource.com/c/go/+/481983 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Michael Knyszek <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]>
1 parent d6759e7 commit ef41a4e

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

src/mime/multipart/formdata.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
8484
maxMemoryBytes = math.MaxInt64
8585
}
8686
}
87+
var copyBuf []byte
8788
for {
8889
p, err := r.nextPart(false, maxMemoryBytes)
8990
if err == io.EOF {
@@ -147,14 +148,22 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
147148
}
148149
}
149150
numDiskFiles++
150-
size, err := io.Copy(file, io.MultiReader(&b, p))
151+
if _, err := file.Write(b.Bytes()); err != nil {
152+
return nil, err
153+
}
154+
if copyBuf == nil {
155+
copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses
156+
}
157+
// os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it.
158+
type writerOnly struct{ io.Writer }
159+
remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf)
151160
if err != nil {
152161
return nil, err
153162
}
154163
fh.tmpfile = file.Name()
155-
fh.Size = size
164+
fh.Size = int64(b.Len()) + remainingSize
156165
fh.tmpoff = fileOff
157-
fileOff += size
166+
fileOff += fh.Size
158167
if !combineFiles {
159168
if err := file.Close(); err != nil {
160169
return nil, err

src/mime/multipart/formdata_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,52 @@ func testReadFormManyFiles(t *testing.T, distinct bool) {
368368
t.Fatalf("temp dir contains %v files; want 0", len(names))
369369
}
370370
}
371+
372+
func BenchmarkReadForm(b *testing.B) {
373+
for _, test := range []struct {
374+
name string
375+
form func(fw *Writer, count int)
376+
}{{
377+
name: "fields",
378+
form: func(fw *Writer, count int) {
379+
for i := 0; i < count; i++ {
380+
w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i))
381+
fmt.Fprintf(w, "value %v", i)
382+
}
383+
},
384+
}, {
385+
name: "files",
386+
form: func(fw *Writer, count int) {
387+
for i := 0; i < count; i++ {
388+
w, _ := fw.CreateFormFile(fmt.Sprintf("field%v", i), fmt.Sprintf("file%v", i))
389+
fmt.Fprintf(w, "value %v", i)
390+
}
391+
},
392+
}} {
393+
b.Run(test.name, func(b *testing.B) {
394+
for _, maxMemory := range []int64{
395+
0,
396+
1 << 20,
397+
} {
398+
var buf bytes.Buffer
399+
fw := NewWriter(&buf)
400+
test.form(fw, 10)
401+
if err := fw.Close(); err != nil {
402+
b.Fatal(err)
403+
}
404+
b.Run(fmt.Sprintf("maxMemory=%v", maxMemory), func(b *testing.B) {
405+
b.ReportAllocs()
406+
for i := 0; i < b.N; i++ {
407+
fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary())
408+
form, err := fr.ReadForm(maxMemory)
409+
if err != nil {
410+
b.Fatal(err)
411+
}
412+
form.RemoveAll()
413+
}
414+
415+
})
416+
}
417+
})
418+
}
419+
}

0 commit comments

Comments
 (0)