fix: reject duplicate part numbers in CompleteMultipartUpload

sort.SliceIsSorted with a strict '<' predicate treats equal neighbours
as sorted, so a completion list like [1,1] passed the order check and
the same part was assembled into the object twice: a single uploaded
5 MiB part produced a 10 MiB object. Replace the check with an explicit
strictly-increasing scan that rejects repeats with InvalidPartOrder
before anything is assembled. Gaps and lists not starting at part 1
remain legal, matching AWS semantics.

The regression test drives the real CompleteMultipartUpload handler on
both Erasure backends and asserts that rejected completions leave no
object behind and keep the upload retryable.

Closes #49

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-03 22:18:10 +08:00
parent 38366f6543
commit 22c1e41fd2
2 changed files with 276 additions and 6 deletions
+9 -6
View File
@@ -26,7 +26,6 @@ import (
"net/http"
"net/textproto"
"net/url"
"sort"
"strconv"
"strings"
"time"
@@ -967,11 +966,15 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
return
}
if !sort.SliceIsSorted(complMultipartUpload.Parts, func(i, j int) bool {
return complMultipartUpload.Parts[i].PartNumber < complMultipartUpload.Parts[j].PartNumber
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartOrder), r.URL)
return
// The parts list must be strictly increasing by part number. Gaps are
// allowed, repeats are not - sort.SliceIsSorted() with a '<' predicate
// considers equal neighbours sorted, so it is checked explicitly here,
// before anything is assembled into the target object.
for i := 1; i < len(complMultipartUpload.Parts); i++ {
if complMultipartUpload.Parts[i-1].PartNumber >= complMultipartUpload.Parts[i].PartNumber {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidPartOrder), r.URL)
return
}
}
// Reject retention or governance headers if set, CompleteMultipartUpload spec