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:
@@ -1250,12 +1250,15 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
|
||||
|
||||
// An ILM expiry is decided on the versions in one pool. Under the lock,
|
||||
// check it with the versions in all pools; the sets then take it as
|
||||
// decided, without a recheck by the versions each one holds.
|
||||
// decided, without a recheck by the versions each one holds. They only
|
||||
// keep a version whose protection they read differently.
|
||||
if z.expiryNeedsAllPools(opts) {
|
||||
if err = z.checkExpiryAllPools(ctx, bucket, object, opts); err != nil {
|
||||
var approved objectLockState
|
||||
if approved, err = z.checkExpiryAllPools(ctx, bucket, object, opts); err != nil {
|
||||
return ObjectInfo{Name: decodeDirObject(object), VersionID: opts.VersionID}, err
|
||||
}
|
||||
opts.Expiration.CheckedAllPools = true
|
||||
opts.Expiration.ApprovedProtection = approved
|
||||
}
|
||||
|
||||
if opts.DeletePrefix {
|
||||
@@ -1527,6 +1530,7 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
batch = make([]ObjectToDelete, 0, len(objects))
|
||||
batchIdx = make([]int, 0, len(objects))
|
||||
data, dataIdx := make([]ObjectToDelete, 0, len(objects)), make([]int, 0, len(objects))
|
||||
approved := make([]objectLockState, 0, len(objects))
|
||||
ilm := make(map[string]lifecycleAllPools)
|
||||
for i, obj := range objects {
|
||||
if derrs[i] != nil {
|
||||
@@ -1536,6 +1540,7 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
// its object in all pools, see checkExpiryAllPools. When the
|
||||
// expiry no longer applies, nothing is deleted and nothing is
|
||||
// left to replicate.
|
||||
var protection objectLockState
|
||||
vopts := ObjectOptions{Expiration: opts.Expiration, VersionID: obj.VersionID}
|
||||
if obj.VersionID != "" && z.expiryNeedsAllPools(vopts) {
|
||||
all, ok := ilm[obj.ObjectName]
|
||||
@@ -1548,6 +1553,7 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
derrs[i] = err
|
||||
continue
|
||||
}
|
||||
protection = all.protection(obj.VersionID)
|
||||
}
|
||||
if deletesData(obj, opts) {
|
||||
if z.copyWithoutDeleteQuorum(ctx, bucket, obj.ObjectName, obj.VersionID) {
|
||||
@@ -1556,13 +1562,14 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
}
|
||||
data = append(data, obj)
|
||||
dataIdx = append(dataIdx, i)
|
||||
approved = append(approved, protection)
|
||||
continue
|
||||
}
|
||||
batch = append(batch, obj)
|
||||
batchIdx = append(batchIdx, i)
|
||||
}
|
||||
if len(data) > 0 {
|
||||
deleted, errs := z.deleteObjectsReconciled(ctx, bucket, data, opts)
|
||||
deleted, errs := z.deleteObjectsReconciled(ctx, bucket, data, approved, opts)
|
||||
for j, i := range dataIdx {
|
||||
dobjects[i], derrs[i] = deleted[j], errs[j]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user