Skip to content

Commit 53b4360

Browse files
neildgopherbot
authored andcommitted
[release-branch.go1.20] mime/multipart: limit memory/inode consumption of ReadForm
Reader.ReadForm is documented as storing "up to maxMemory bytes + 10MB" in memory. Parsed forms can consume substantially more memory than this limit, since ReadForm does not account for map entry overhead and MIME headers. In addition, while the amount of disk memory consumed by ReadForm can be constrained by limiting the size of the parsed input, ReadForm will create one temporary file per form part stored on disk, potentially consuming a large number of inodes. Update ReadForm's memory accounting to include part names, MIME headers, and map entry overhead. Update ReadForm to store all on-disk file parts in a single temporary file. Files returned by FileHeader.Open are documented as having a concrete type of *os.File when a file is stored on disk. The change to use a single temporary file for all parts means that this is no longer the case when a form contains more than a single file part stored on disk. The previous behavior of storing each file part in a separate disk file may be reenabled with GODEBUG=multipartfiles=distinct. Update Reader.NextPart and Reader.NextRawPart to set a 10MiB cap on the size of MIME headers. Thanks to Jakob Ackermann (@das7pad) for reporting this issue. Updates #58006 Fixes #58363 Fixes CVE-2022-41725 Change-Id: Ibd780a6c4c83ac8bcfd3cbe344f042e9940f2eab Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1714276 Reviewed-by: Julie Qiu <[email protected]> TryBot-Result: Security TryBots <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Run-TryBot: Damien Neil <[email protected]> (cherry picked from commit 7d0da0029bfbe3228cc5216ced8c7b3184eb517d) Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1728950 Reviewed-by: Damien Neil <[email protected]> Run-TryBot: Roland Shoemaker <[email protected]> Reviewed-by: Tatiana Bradley <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/468120 Auto-Submit: Michael Pratt <[email protected]> Run-TryBot: Michael Pratt <[email protected]> Reviewed-by: Than McIntosh <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent bdf07c2 commit 53b4360

File tree

6 files changed

+297
-38
lines changed

6 files changed

+297
-38
lines changed

src/mime/multipart/formdata.go

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package multipart
77
import (
88
"bytes"
99
"errors"
10+
"internal/godebug"
1011
"io"
1112
"math"
1213
"net/textproto"
@@ -31,25 +32,61 @@ func (r *Reader) ReadForm(maxMemory int64) (*Form, error) {
3132
return r.readForm(maxMemory)
3233
}
3334

35+
var multipartFiles = godebug.New("multipartfiles")
36+
3437
func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
3538
form := &Form{make(map[string][]string), make(map[string][]*FileHeader)}
39+
var (
40+
file *os.File
41+
fileOff int64
42+
)
43+
numDiskFiles := 0
44+
combineFiles := multipartFiles.Value() != "distinct"
3645
defer func() {
46+
if file != nil {
47+
if cerr := file.Close(); err == nil {
48+
err = cerr
49+
}
50+
}
51+
if combineFiles && numDiskFiles > 1 {
52+
for _, fhs := range form.File {
53+
for _, fh := range fhs {
54+
fh.tmpshared = true
55+
}
56+
}
57+
}
3758
if err != nil {
3859
form.RemoveAll()
60+
if file != nil {
61+
os.Remove(file.Name())
62+
}
3963
}
4064
}()
4165

42-
// Reserve an additional 10 MB for non-file parts.
43-
maxValueBytes := maxMemory + int64(10<<20)
44-
if maxValueBytes <= 0 {
66+
// maxFileMemoryBytes is the maximum bytes of file data we will store in memory.
67+
// Data past this limit is written to disk.
68+
// This limit strictly applies to content, not metadata (filenames, MIME headers, etc.),
69+
// since metadata is always stored in memory, not disk.
70+
//
71+
// maxMemoryBytes is the maximum bytes we will store in memory, including file content,
72+
// non-file part values, metdata, and map entry overhead.
73+
//
74+
// We reserve an additional 10 MB in maxMemoryBytes for non-file data.
75+
//
76+
// The relationship between these parameters, as well as the overly-large and
77+
// unconfigurable 10 MB added on to maxMemory, is unfortunate but difficult to change
78+
// within the constraints of the API as documented.
79+
maxFileMemoryBytes := maxMemory
80+
maxMemoryBytes := maxMemory + int64(10<<20)
81+
if maxMemoryBytes <= 0 {
4582
if maxMemory < 0 {
46-
maxValueBytes = 0
83+
maxMemoryBytes = 0
4784
} else {
48-
maxValueBytes = math.MaxInt64
85+
maxMemoryBytes = math.MaxInt64
4986
}
5087
}
5188
for {
52-
p, err := r.NextPart()
89+
p, err := r.nextPart(false, maxMemoryBytes)
5390
if err == io.EOF {
5491
break
5592
}
@@ -63,59 +100,91 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
63100
}
64101
filename := p.FileName()
65102

103+
// Multiple values for the same key (one map entry, longer slice) are cheaper
104+
// than the same number of values for different keys (many map entries), but
105+
// using a consistent per-value cost for overhead is simpler.
106+
maxMemoryBytes -= int64(len(name))
107+
maxMemoryBytes -= 100 // map overhead
108+
if maxMemoryBytes < 0 {
109+
// We can't actually take this path, since nextPart would already have
110+
// rejected the MIME headers for being too large. Check anyway.
111+
return nil, ErrMessageTooLarge
112+
}
113+
66114
var b bytes.Buffer
67115

68116
if filename == "" {
69117
// value, store as string in memory
70-
n, err := io.CopyN(&b, p, maxValueBytes+1)
118+
n, err := io.CopyN(&b, p, maxMemoryBytes+1)
71119
if err != nil && err != io.EOF {
72120
return nil, err
73121
}
74-
maxValueBytes -= n
75-
if maxValueBytes < 0 {
122+
maxMemoryBytes -= n
123+
if maxMemoryBytes < 0 {
76124
return nil, ErrMessageTooLarge
77125
}
78126
form.Value[name] = append(form.Value[name], b.String())
79127
continue
80128
}
81129

82130
// file, store in memory or on disk
131+
maxMemoryBytes -= mimeHeaderSize(p.Header)
132+
if maxMemoryBytes < 0 {
133+
return nil, ErrMessageTooLarge
134+
}
83135
fh := &FileHeader{
84136
Filename: filename,
85137
Header: p.Header,
86138
}
87-
n, err := io.CopyN(&b, p, maxMemory+1)
139+
n, err := io.CopyN(&b, p, maxFileMemoryBytes+1)
88140
if err != nil && err != io.EOF {
89141
return nil, err
90142
}
91-
if n > maxMemory {
92-
// too big, write to disk and flush buffer
93-
file, err := os.CreateTemp("", "multipart-")
94-
if err != nil {
95-
return nil, err
143+
if n > maxFileMemoryBytes {
144+
if file == nil {
145+
file, err = os.CreateTemp(r.tempDir, "multipart-")
146+
if err != nil {
147+
return nil, err
148+
}
96149
}
150+
numDiskFiles++
97151
size, err := io.Copy(file, io.MultiReader(&b, p))
98-
if cerr := file.Close(); err == nil {
99-
err = cerr
100-
}
101152
if err != nil {
102-
os.Remove(file.Name())
103153
return nil, err
104154
}
105155
fh.tmpfile = file.Name()
106156
fh.Size = size
157+
fh.tmpoff = fileOff
158+
fileOff += size
159+
if !combineFiles {
160+
if err := file.Close(); err != nil {
161+
return nil, err
162+
}
163+
file = nil
164+
}
107165
} else {
108166
fh.content = b.Bytes()
109167
fh.Size = int64(len(fh.content))
110-
maxMemory -= n
111-
maxValueBytes -= n
168+
maxFileMemoryBytes -= n
169+
maxMemoryBytes -= n
112170
}
113171
form.File[name] = append(form.File[name], fh)
114172
}
115173

116174
return form, nil
117175
}
118176

177+
func mimeHeaderSize(h textproto.MIMEHeader) (size int64) {
178+
for k, vs := range h {
179+
size += int64(len(k))
180+
size += 100 // map entry overhead
181+
for _, v := range vs {
182+
size += int64(len(v))
183+
}
184+
}
185+
return size
186+
}
187+
119188
// Form is a parsed multipart form.
120189
// Its File parts are stored either in memory or on disk,
121190
// and are accessible via the *FileHeader's Open method.
@@ -133,7 +202,7 @@ func (f *Form) RemoveAll() error {
133202
for _, fh := range fhs {
134203
if fh.tmpfile != "" {
135204
e := os.Remove(fh.tmpfile)
136-
if e != nil && err == nil {
205+
if e != nil && !errors.Is(e, os.ErrNotExist) && err == nil {
137206
err = e
138207
}
139208
}
@@ -148,15 +217,25 @@ type FileHeader struct {
148217
Header textproto.MIMEHeader
149218
Size int64
150219

151-
content []byte
152-
tmpfile string
220+
content []byte
221+
tmpfile string
222+
tmpoff int64
223+
tmpshared bool
153224
}
154225

155226
// Open opens and returns the FileHeader's associated File.
156227
func (fh *FileHeader) Open() (File, error) {
157228
if b := fh.content; b != nil {
158229
r := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b)))
159-
return sectionReadCloser{r}, nil
230+
return sectionReadCloser{r, nil}, nil
231+
}
232+
if fh.tmpshared {
233+
f, err := os.Open(fh.tmpfile)
234+
if err != nil {
235+
return nil, err
236+
}
237+
r := io.NewSectionReader(f, fh.tmpoff, fh.Size)
238+
return sectionReadCloser{r, f}, nil
160239
}
161240
return os.Open(fh.tmpfile)
162241
}
@@ -175,8 +254,12 @@ type File interface {
175254

176255
type sectionReadCloser struct {
177256
*io.SectionReader
257+
io.Closer
178258
}
179259

180260
func (rc sectionReadCloser) Close() error {
261+
if rc.Closer != nil {
262+
return rc.Closer.Close()
263+
}
181264
return nil
182265
}

0 commit comments

Comments
 (0)