mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 17:45:32 +03:00
fix(ilm): require an authoritative cross-pool history before lifecycle expiry
The merged all-pool check from #279 decided on the versions that a read quorum of each set merged non-strictly. A drive that missed one metadata update could therefore supply the revision used for the decision, and a partially unreadable set could drop a committed version from the history. Both let an expiry delete data the rule did not cover, including a version under legal hold. - objectVersions accepts a revision only when max(n/2+1, the version's metadata read quorum) drives hold it, identifies revisions by the header plus a sha256 over a canonical, length-prefixed encoding of the normalized metadata, filters non-accepted revisions before the strict merge, and fails closed on omitted versions, too many unreadable drives, or cancellation. - Lock fields are read with the evaluator's key precedence. Cross-pool copies that disagree on hold or retention must be strictly ordered by valid timestamps; copies with different ModTime are separate writes and an older protected write refuses the object. The set safeguard re-checks protection and pending purges on its fresh read for checked version deletes. - Duplicate copies with semantically different replication or purge state refuse the object. - Batch expiry announces and audits only completed or already-absent removals. Reviewed adversarially over seven rounds; the final round approved with no remaining findings in scope. All-versions expiry through deletePrefix and tiered delete-all dispatch are tracked separately. Signed-off-by: Feng Ruohang <rh@vonng.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -2118,6 +2118,11 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
return ObjectInfo{}, toObjectErr(er.deletePrefix(ctx, bucket, object), bucket, object)
|
||||
}
|
||||
|
||||
// An ILM version expiry that the pools checked with the versions in all
|
||||
// pools; the set still guards the protection it reads, see
|
||||
// checkedExpiryRefused.
|
||||
checkedExpiry := opts.Expiration.Expire && opts.Expiration.CheckedAllPools && opts.VersionID != "" && !opts.Transition.ExpireRestored
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
versionFound := true
|
||||
objInfo = ObjectInfo{VersionID: opts.VersionID} // version id needed in Delete API response.
|
||||
@@ -2126,8 +2131,9 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
if gerr != nil && goi.Name == "" {
|
||||
if _, ok := gerr.(InsufficientReadQuorum); ok {
|
||||
// A restored-copy expiry needs fresh metadata to confirm its target
|
||||
// and deadline; ordinary unversioned DELETE may still try a blind delete.
|
||||
if opts.Transition.ExpireRestored || opts.Versioned || opts.VersionSuspended || countOnlineDisks(storageDisks) < len(storageDisks)/2+1 {
|
||||
// and deadline, a checked expiry to guard its protection; ordinary
|
||||
// unversioned DELETE may still try a blind delete.
|
||||
if opts.Transition.ExpireRestored || checkedExpiry || opts.Versioned || opts.VersionSuspended || countOnlineDisks(storageDisks) < len(storageDisks)/2+1 {
|
||||
// Add an MRF heal for next time.
|
||||
er.addPartial(bucket, object, opts.VersionID)
|
||||
return objInfo, InsufficientWriteQuorum{}
|
||||
@@ -2197,6 +2203,8 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if checkedExpiry && checkedExpiryRefused(*lc, rcfg, replcfg, goi, opts.Expiration.ApprovedProtection) {
|
||||
return goi, errLifecycleNotApplicable
|
||||
}
|
||||
|
||||
// Determine whether to mark object deleted for replication
|
||||
|
||||
Reference in New Issue
Block a user