filer: bound zip spool memory by actual bytes

A chunked upload has no Content-Length, so the old size hint let an unbounded body go through io.ReadAll. Read up to the memory threshold plus one byte first, then spill the prefix and the rest to a temp file, capped at ZipExtractMaxBytes total.
This commit is contained in:
Chris Lu
2026-10-16 01:20:15 -07:00
parent cb6e7f4d24
commit 3b7c6b642d
3 changed files with 23 additions and 14 deletions
+19 -10
View File
@@ -20,17 +20,17 @@ const (
// SpoolZipReader buffers r into a seekable source for zip.NewReader: memory for
// small archives, a temp file otherwise. The returned cleanup releases it.
func SpoolZipReader(r io.Reader, size int64) (ra io.ReaderAt, n int64, cleanup func(), err error) {
func SpoolZipReader(r io.Reader) (ra io.ReaderAt, n int64, cleanup func(), err error) {
nop := func() {}
if size >= 0 && size <= zipSpoolMemoryLimit {
var buf []byte
if buf, err = io.ReadAll(r); err != nil {
return nil, 0, nop, err
}
return bytes.NewReader(buf), int64(len(buf)), nop, nil
head, err := io.ReadAll(io.LimitReader(r, zipSpoolMemoryLimit+1))
if err != nil {
return nil, 0, nop, err
}
var f *os.File
if f, err = os.CreateTemp("", "seaweedfs-extract-*.zip"); err != nil {
if int64(len(head)) <= zipSpoolMemoryLimit {
return bytes.NewReader(head), int64(len(head)), nop, nil
}
f, err := os.CreateTemp("", "seaweedfs-extract-*.zip")
if err != nil {
return nil, 0, nop, err
}
cleanup = func() {
@@ -38,7 +38,16 @@ func SpoolZipReader(r io.Reader, size int64) (ra io.ReaderAt, n int64, cleanup f
f.Close()
os.Remove(name)
}
if n, err = io.Copy(f, r); err != nil {
if _, err = f.Write(head); err == nil {
var rest int64
if rest, err = io.Copy(f, io.LimitReader(r, ZipExtractMaxBytes+1)); err == nil {
n = int64(len(head)) + rest
}
}
if err == nil && n > ZipExtractMaxBytes {
err = fmt.Errorf("zip archive exceeds %d bytes", ZipExtractMaxBytes)
}
if err != nil {
cleanup()
return nil, 0, nop, err
}
+3 -3
View File
@@ -47,7 +47,7 @@ func TestSanitizeZipMemberName(t *testing.T) {
func TestSpoolZipReaderMemory(t *testing.T) {
data := []byte(strings.Repeat("x", 1024))
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(data), int64(len(data)))
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(data))
if err != nil {
t.Fatalf("spool: %v", err)
}
@@ -66,7 +66,7 @@ func TestSpoolZipReaderMemory(t *testing.T) {
func TestSpoolZipReaderTempFile(t *testing.T) {
data := bytes.Repeat([]byte("y"), zipSpoolMemoryLimit+1)
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(data), int64(len(data)))
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(data))
if err != nil {
t.Fatalf("spool: %v", err)
}
@@ -113,7 +113,7 @@ func TestSpoolZipReaderRoundTrip(t *testing.T) {
w.Write([]byte("hello"))
zw.Close()
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
ra, n, cleanup, err := SpoolZipReader(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("spool: %v", err)
}
+1 -1
View File
@@ -58,7 +58,7 @@ func (fs *FilerServer) extractZipArchive(ctx context.Context, w http.ResponseWri
}
}
ra, size, cleanup, err := filer.SpoolZipReader(reader, contentLength)
ra, size, cleanup, err := filer.SpoolZipReader(reader)
if err != nil {
writeJsonError(w, r, http.StatusInternalServerError, fmt.Errorf("read input: %w", err))
return