fix(restore): preserve versions across cleanup and failed reads

Reject replacement markers and legacy objects before restored-copy cleanup
can mutate their type or data. Prevent restore lookups and failure cleanup
from purging dangling versions, preserve Transition on metadata rereads,
and return without writing when the initial restore read fails.

Add real-disk regressions for stale null tasks, minority metadata, initial
and cleanup read failures, and stored marker/legacy types. Keep ordinary
dangling cleanup and marker purge as positive controls.

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-03 22:32:33 +08:00
parent e7103d77cb
commit 8579466557
4 changed files with 318 additions and 12 deletions
+15 -2
View File
@@ -509,6 +509,11 @@ func joinErrs(errs []error) string {
}
func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int, opts ObjectOptions) (FileInfo, error) {
// Restore and restored-copy cleanup must preserve the version on read
// failure, including lookups and failure cleanup that reread metadata.
if opts.Transition.ExpireRestored || opts.Transition.RestoreRequest != nil {
return FileInfo{}, errErasureReadQuorum
}
m, ok := isObjectDangling(metaArr, errs, dataErrsByPart)
if !ok {
// We only come here if we cannot figure out if the object
@@ -2136,6 +2141,12 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
}
}
// A queued null restore may now address a replacement delete marker.
// Such errors carry ObjectInfo, but never authorize cache cleanup.
if opts.Transition.ExpireRestored && (gerr != nil || goi.DeleteMarker) {
return objInfo, VersionNotFound{Bucket: bucket, Object: object, VersionID: opts.VersionID}
}
if opts.EvalMetadataFn != nil {
dsc, err := opts.EvalMetadataFn(&goi, gerr)
if err != nil {
@@ -2641,7 +2652,8 @@ func (er erasureObjects) updateRestoreMetadata(ctx context.Context, bucket, obje
delete(oi.UserDefined, xhttp.AmzRestore)
if _, err := er.CopyObject(ctx, bucket, object, bucket, object, oi, ObjectOptions{
VersionID: oi.VersionID,
VersionID: oi.VersionID,
Transition: opts.Transition,
}, ObjectOptions{
VersionID: oi.VersionID,
}); err != nil {
@@ -2665,7 +2677,8 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
// get the file info on disk for transitioned object
actualfi, _, _, err := er.getObjectFileInfo(ctx, bucket, object, opts, false)
if err != nil {
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
// No version was read, so there is no restore metadata to update.
return toObjectErr(err, bucket, object)
}
oi = actualfi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)