fix(heal): classify xl.meta checksum failures as storage corruption

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-07 00:23:45 +08:00
parent 96b3a19d0a
commit 98fd7004e8
2 changed files with 46 additions and 8 deletions
@@ -18,11 +18,13 @@
package cmd package cmd
import ( import (
"bytes"
"errors" "errors"
"slices" "slices"
"strings" "strings"
"testing" "testing"
madmin "github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/bucket/lifecycle" "github.com/minio/minio/internal/bucket/lifecycle"
) )
@@ -136,8 +138,8 @@ func TestPurgeAllVersionsRefusalReasons(t *testing.T) {
} }
// A drive whose metadata does not decode refuses the purge, as an // A drive whose metadata does not decode refuses the purge, as an
// unreadable one does. Object heal does not rewrite such metadata; once the // unreadable one does. Healing each surviving version restores the metadata
// drive holds readable metadata again, the next purge removes the key. // from a quorum before the next purge can remove the key.
func TestPurgeAllVersionsRefusesCorruptDrive(t *testing.T) { func TestPurgeAllVersionsRefusesCorruptDrive(t *testing.T) {
z := purgeSinglePool(t, 16) z := purgeSinglePool(t, 16)
p := newPurgeTest(t, z, "purge-corrupt", MakeBucketOptions{VersioningEnabled: true}, purgeRules) p := newPurgeTest(t, z, "purge-corrupt", MakeBucketOptions{VersioningEnabled: true}, purgeRules)
@@ -156,14 +158,20 @@ func TestPurgeAllVersionsRefusesCorruptDrive(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
buf := slices.Clone(good) buf := slices.Clone(good)
buf[len(buf)/2] ^= 0xff meta, _, err := isIndexedMetaV2(buf)
if err != nil {
t.Fatal(err)
}
meta[len(meta)-1] ^= 1
if err := bad.WriteAll(ctx, p.bucket, metaPath, buf); err != nil { if err := bad.WriteAll(ctx, p.bucket, metaPath, buf); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if raw, err := bad.ReadXL(ctx, p.bucket, key, false); err != nil { if raw, err := bad.ReadXL(ctx, p.bucket, key, false); err != nil {
t.Fatalf("the corrupted metadata is not read: %v", err) t.Fatalf("the corrupted metadata is not read: %v", err)
} else if err := (&xlMetaV2{}).LoadOrConvert(raw.Buf); err == nil { } else if err := (&xlMetaV2{}).LoadOrConvert(raw.Buf); !errors.Is(err, errFileCorrupt) {
t.Fatal("the corrupted metadata still decodes") t.Fatalf("CRC classification: %v", err)
} else if got := toStorageErr(errors.New(err.Error())); got != errFileCorrupt {
t.Fatalf("CRC classification after storage RPC: %v", got)
} }
delta := incompleteDelta(func() { p.run(ops) }) delta := incompleteDelta(func() { p.run(ops) })
@@ -179,9 +187,37 @@ func TestPurgeAllVersionsRefusesCorruptDrive(t *testing.T) {
t.Fatalf("the refused purge left %s readable", got) t.Fatalf("the refused purge left %s readable", got)
} }
if err := bad.WriteAll(ctx, p.bucket, metaPath, good); err != nil { versions, err := getFileInfoVersions(good, p.bucket, key, false)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, mode := range []madmin.HealScanMode{madmin.HealNormalScan, madmin.HealDeepScan} {
if err := bad.WriteAll(ctx, p.bucket, metaPath, buf); err != nil {
t.Fatal(err)
}
if _, err := z.HealObject(ctx, p.bucket, key, nullVersionID, madmin.HealOpts{DryRun: true, ScanMode: mode}); err != nil {
t.Fatal(err)
}
unchanged, err := bad.ReadAll(ctx, p.bucket, metaPath)
if err != nil || !bytes.Equal(unchanged, buf) {
t.Fatalf("dry-run changed corrupt metadata: %v", err)
}
for _, fi := range versions.Versions {
id := fi.VersionID
if id == "" {
id = nullVersionID
}
if _, err := z.HealObject(ctx, p.bucket, key, id, madmin.HealOpts{ScanMode: mode}); err != nil {
t.Fatalf("heal %s: %v", id, err)
}
if _, err := bad.ReadVersion(ctx, "", p.bucket, key, id, ReadOptions{}); err != nil {
t.Fatalf("repaired version %s unreadable: %v", id, err)
}
}
if got := p.get(key); got != "v2" {
t.Fatalf("heal changed latest data: %s", got)
}
}
p.run([]expiryOp{p.deleteAllTask(0, key, lifecycle.DeleteAllVersionsAction)}) p.run([]expiryOp{p.deleteAllTask(0, key, lifecycle.DeleteAllVersionsAction)})
if got := p.held(0, key); got != nil { if got := p.held(0, key); got != nil {
t.Errorf("the purge after the repair left %v", got) t.Errorf("the purge after the repair left %v", got)
+4 -2
View File
@@ -878,7 +878,9 @@ func isIndexedMetaV2(buf []byte) (meta xlMetaBuf, data xlMetaInlineData, err err
// Read metadata CRC // Read metadata CRC
buf = nbuf buf = nbuf
if got := uint32(xxhash.Sum64(meta)); got != crc { if got := uint32(xxhash.Sum64(meta)); got != crc {
return nil, nil, fmt.Errorf("xlMetaV2.Load version(%d), CRC mismatch, want 0x%x, got 0x%x", minor, crc, got) // Use the storage sentinel so corruption retains its classification
// across both local storage and storage RPC.
return nil, nil, errFileCorrupt
} }
} else { } else {
return nil, nil, err return nil, nil, err
@@ -1054,7 +1056,7 @@ func (x *xlMetaV2) loadLegacy(buf []byte) error {
// Read metadata CRC (added in v2) // Read metadata CRC (added in v2)
buf = nbuf buf = nbuf
if got := uint32(xxhash.Sum64(v)); got != crc { if got := uint32(xxhash.Sum64(v)); got != crc {
return fmt.Errorf("xlMetaV2.Load version(%d), CRC mismatch, want 0x%x, got 0x%x", minor, crc, got) return errFileCorrupt
} }
} else { } else {
return fmt.Errorf("xlMetaV2.Load version(%d), loading CRC: %w", minor, err) return fmt.Errorf("xlMetaV2.Load version(%d), loading CRC: %w", minor, err)