Files
versitygw/backend/meta/none.go
T
Ben McClelland ef8f1b987f fix: eliminate sidecar metadata race on concurrent uploads
STILL NOT FIXED:
in backend/meta/sidecar.go CommitMetadata()
Between pathIno(dataPath) != myIno returning false (check passes) and os.Rename executing, another goroutine can win link() and install a new inode at dataPath. This goroutine then proceeds to rename its stale attributes over the winner's freshly committed ones.

The comment's safety argument — "the true winner's CommitMetadata will overwrite any partially committed attributes before it finishes" — only holds if the winner runs entirely after this goroutine's renames. If the winner already finished its renames before this goroutine's stale os.Rename executes, the loser's data corrupts the final metadata silently.

This is a fundamental TOCTOU problem. There is no POSIX syscall for "rename only if this inode still owns the destination", so it can't be fixed with filesystem operations alone. The real fix would require serializing CommitMetadata calls per object

----

Previously, StoreAttribute wrote metadata (checksums, etc.) directly to
the final per-object sidecar path during upload. Concurrent uploads of
the same object would race to write into the same directory, causing one
upload's metadata to be silently overwritten by another's data file, or
vice-versa. On Linux, O_TMPFILE fd-number reuse made this worse: after
link() closed the fd, the inode-number slot could be immediately reused
by a different goroutine.

Fix by staging sidecar metadata in a per-upload temporary directory
named .sgwtmp.<pid>.<inode> instead of the final path, then atomically
committing it after the data file has been linked.

tmpSidecarID() identifies an in-flight upload by PID + inode (Unix) or
PID + temp filename (Windows), matching the token produced by
tmpfile.SidecarToken() in the posix backend so staging and commit find
the same directory.

StoreAttribute now writes attributes into the inode-keyed temp sidecar
dir with a retry loop (up to 5 attempts) to handle the inode-reuse edge
case where a concurrent CommitMetadata RemoveAll races with a fresh
MkdirAll on the same directory name.

CommitMetadata moves the staged attributes to the final object path
one-by-one via atomic rename. Before each rename it re-verifies that the
data file's inode still matches the token; if not, a later concurrent
upload won the race and this goroutine aborts cleanly, leaving the
winner's metadata intact.
2026-06-10 11:44:32 -07:00

70 lines
2.2 KiB
Go

// Copyright 2025 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package meta
import (
"os"
)
// NoMeta is a metadata storer that does not store metadata.
// This can be useful for read only mounts where attempting to store metadata
// would fail.
type NoMeta struct{}
// RetrieveAttribute retrieves the value of a specific attribute for an object or a bucket.
// always returns ErrNoSuchKey
func (NoMeta) RetrieveAttribute(_ *os.File, _, _, _ string) ([]byte, error) {
return nil, ErrNoSuchKey
}
// StoreAttribute stores the value of a specific attribute for an object or a bucket.
// always returns nil without storing the attribute
func (NoMeta) StoreAttribute(_ *os.File, _, _, _ string, _ []byte) error {
return nil
}
// DeleteAttribute removes the value of a specific attribute for an object or a bucket.
// always returns nil without deleting the attribute
func (NoMeta) DeleteAttribute(_, _, _ string) error {
return nil
}
// ListAttributes lists all attributes for an object or a bucket.
// always returns an empty list of attributes
func (NoMeta) ListAttributes(_, _ string) ([]string, error) {
return []string{}, nil
}
// DeleteAttributes removes all attributes for an object or a bucket.
// always returns nil without deleting any attributes
func (NoMeta) DeleteAttributes(bucket, object string) error {
return nil
}
// CommitMetadata is a no-op because NoMeta does not store any metadata.
func (NoMeta) CommitMetadata(_, _, _, _ string) error {
return nil
}
// CleanupMetadata is a no-op because NoMeta does not store any metadata.
func (NoMeta) CleanupMetadata(_, _ string) error {
return nil
}
// RenameObject is a no-op because NoMeta does not store metadata.
func (NoMeta) RenameObject(_, _, _ string) error {
return nil
}