mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 17:45:32 +03:00
fix(pools): check copy names before locking them
CopyObject locks its source and destination before it reads the source, and lockObjects merged two requests only when their raw names were equal. A lock is taken on the cleaned name, so an invalid source such as a//x or a/./x and the destination a/x asked for a read lock and a write lock on one resource: the copy waited on itself until the operation timeout and answered 503, holding the key's lock in short windows meanwhile. Before the lock-domain change the invalid source was refused at once, when the source was read. This reached single nodes, multi-pool deployments and single pools whose sets share hosts. - Check the source with checkGetObjArgs before any lock, the first check and the first error the release reported. Lock the destination only when its name is valid. An invalid destination fails at the copy's own destination check before it writes, after a missing source (404) or a failed source precondition (412), as before. - lockObjects merges requests by the resource the lockers see, the cleaned bucket/object, within one lock table: the object's set where the sets of a pool have their own lockers, else one table. This keeps aliases on a shared local table from conflicting and never merges requests of different distributed domains. Tests: aliased sources and destinations are refused with 400 at once in every lock layout and two pools; an invalid destination keeps the 404 and 412 precedence; lockObjects merges an alias with its clean name. The alias and merge tests fail without the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -87,27 +87,34 @@ type objectLockRequest struct {
|
||||
func (z *erasureServerPools) lockObjects(ctx context.Context, reqs ...objectLockRequest) (LockContext, func(), error) {
|
||||
type entry struct {
|
||||
objectLockRequest
|
||||
domain int
|
||||
domain int
|
||||
resource string
|
||||
}
|
||||
// A lock is one resource in one lock table. The resource is the cleaned
|
||||
// name the lockers see; the domain is the set whose lockers grant it,
|
||||
// or 0 where every set shares one local table or the pools lock
|
||||
// applies.
|
||||
splitDomains := z.SinglePool() && !z.serverPools[0].sharesLocalNSLock()
|
||||
locks := make([]entry, 0, len(reqs))
|
||||
for _, r := range reqs {
|
||||
r.object = encodeDirObject(r.object)
|
||||
i := slices.IndexFunc(locks, func(e entry) bool { return e.bucket == r.bucket && e.object == r.object })
|
||||
domain := 0
|
||||
if splitDomains {
|
||||
domain = z.serverPools[0].getHashedSetIndex(r.object)
|
||||
}
|
||||
resource := pathJoin(r.bucket, r.object)
|
||||
i := slices.IndexFunc(locks, func(e entry) bool { return e.domain == domain && e.resource == resource })
|
||||
if i >= 0 {
|
||||
locks[i].write = locks[i].write || r.write
|
||||
continue
|
||||
}
|
||||
domain := 0
|
||||
if z.SinglePool() {
|
||||
domain = z.serverPools[0].getHashedSetIndex(r.object)
|
||||
}
|
||||
locks = append(locks, entry{objectLockRequest: r, domain: domain})
|
||||
locks = append(locks, entry{objectLockRequest: r, domain: domain, resource: resource})
|
||||
}
|
||||
slices.SortFunc(locks, func(a, b entry) int {
|
||||
if a.domain != b.domain {
|
||||
return a.domain - b.domain
|
||||
}
|
||||
return strings.Compare(pathJoin(a.bucket, a.object), pathJoin(b.bucket, b.object))
|
||||
return strings.Compare(a.resource, b.resource)
|
||||
})
|
||||
nsLocks := make([]nsLockRequest, len(locks))
|
||||
for i, l := range locks {
|
||||
|
||||
Reference in New Issue
Block a user