From 00d4a206f75a6ae5b51bf60e8bcadb2b33662c6d Mon Sep 17 00:00:00 2001 From: Julien Laurenceau Date: Mon, 12 Oct 2026 17:02:02 +0200 Subject: [PATCH] fix: zero EcM when unmarshaling legacy xl.meta V2 headers The legacy header decode paths zeroed EcN twice (z.EcN, z.EcN = 0, 0) instead of EcN and EcM, so EcM retained its previous value when the same structure was reused across decodes. Decoding an xl.meta written by an older release would then leak the erasure metadata (data/parity block counts) of a previously decoded object into it, potentially corrupting quorum and healing decisions. Also zero the fields on the V1 header path, which had no reset at all. Signed-off-by: Julien Laurenceau --- cmd/xl-storage-format-v2-legacy.go | 5 ++- cmd/xl-storage-format-v2_test.go | 66 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/cmd/xl-storage-format-v2-legacy.go b/cmd/xl-storage-format-v2-legacy.go index 9d682487b..188b299d3 100644 --- a/cmd/xl-storage-format-v2-legacy.go +++ b/cmd/xl-storage-format-v2-legacy.go @@ -40,6 +40,7 @@ func (x *xlMetaV2VersionHeader) unmarshalV(v uint8, bts []byte) (o []byte, err e // unmarshalV1 decodes version 1, never released. func (x *xlMetaV2VersionHeader) unmarshalV1(bts []byte) (o []byte, err error) { + x.EcN, x.EcM = 0, 0 var zb0001 uint32 zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { @@ -134,7 +135,7 @@ type xlMetaV2VersionHeaderV2 struct { // UnmarshalMsg implements msgp.Unmarshaler func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, err error) { - z.EcN, z.EcN = 0, 0 + z.EcN, z.EcM = 0, 0 var zb0001 uint32 zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { @@ -184,7 +185,7 @@ func (z *xlMetaV2VersionHeaderV2) UnmarshalMsg(bts []byte) (o []byte, err error) // DecodeMsg implements msgp.Decodable func (z *xlMetaV2VersionHeaderV2) DecodeMsg(dc *msgp.Reader) (err error) { - z.EcN, z.EcN = 0, 0 + z.EcN, z.EcM = 0, 0 var zb0001 uint32 zb0001, err = dc.ReadArrayHeader() if err != nil { diff --git a/cmd/xl-storage-format-v2_test.go b/cmd/xl-storage-format-v2_test.go index 6a9b15a3f..21e76eb76 100644 --- a/cmd/xl-storage-format-v2_test.go +++ b/cmd/xl-storage-format-v2_test.go @@ -38,6 +38,7 @@ import ( "github.com/klauspost/compress/zstd" "github.com/minio/minio/internal/bucket/lifecycle" xhttp "github.com/minio/minio/internal/http" + "github.com/tinylib/msgp/msgp" ) func TestReadXLMetaNoData(t *testing.T) { @@ -1190,3 +1191,68 @@ func BenchmarkToFileInfoNoParts(b *testing.B) { func BenchmarkToFileInfoWithParts(b *testing.B) { benchmarkManyPartsOptionally(b, true) } + +// TestXLMetaV2HeaderLegacyUnmarshalResetsEc checks that decoding legacy +// version headers, which carry no erasure metadata fields, cannot leak +// stale EcN/EcM values from a previously decoded object into the same +// structure. +func TestXLMetaV2HeaderLegacyUnmarshalResetsEc(t *testing.T) { + base := xlMetaV2VersionHeader{ + VersionID: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + ModTime: time.Now().Unix(), + Signature: [4]byte{1, 2, 3, 4}, + Type: ObjectType, + Flags: xlFlagInlineData, + } + + // Legacy V2 header wire format: 5 fields, no EcN/EcM. + var legacy []byte + legacy = msgp.AppendArrayHeader(legacy, 5) + legacy = msgp.AppendBytes(legacy, base.VersionID[:]) + legacy = msgp.AppendInt64(legacy, base.ModTime) + legacy = msgp.AppendBytes(legacy, base.Signature[:]) + legacy = msgp.AppendUint8(legacy, uint8(base.Type)) + legacy = msgp.AppendUint8(legacy, uint8(base.Flags)) + + header := base + header.EcN, header.EcM = 4, 8 + x2 := xlMetaV2VersionHeaderV2{xlMetaV2VersionHeader: &header} + if _, err := x2.UnmarshalMsg(legacy); err != nil { + t.Fatalf("legacy V2 header unmarshal failed: %v", err) + } + if header.EcN != 0 || header.EcM != 0 { + t.Fatalf("stale erasure metadata leaked across objects: EcN %d, EcM %d", header.EcN, header.EcM) + } + + // V1 headers (4 fields, never released) must zero the fields too. + var v1 []byte + v1 = msgp.AppendArrayHeader(v1, 4) + v1 = msgp.AppendBytes(v1, base.VersionID[:]) + v1 = msgp.AppendInt64(v1, base.ModTime) + v1 = msgp.AppendUint8(v1, uint8(base.Type)) + v1 = msgp.AppendUint8(v1, uint8(base.Flags)) + + header = base + header.EcN, header.EcM = 4, 8 + if _, err := header.unmarshalV1(v1); err != nil { + t.Fatalf("legacy V1 header unmarshal failed: %v", err) + } + if header.EcN != 0 || header.EcM != 0 { + t.Fatalf("stale erasure metadata leaked on V1 decode: EcN %d, EcM %d", header.EcN, header.EcM) + } + + // Current headers must keep their erasure metadata. + current := base + current.EcN, current.EcM = 4, 8 + wire, err := current.MarshalMsg(nil) + if err != nil { + t.Fatalf("header marshal failed: %v", err) + } + decoded := base + if _, err := decoded.UnmarshalMsg(wire); err != nil { + t.Fatalf("header unmarshal failed: %v", err) + } + if decoded.EcN != 4 || decoded.EcM != 8 { + t.Fatalf("erasure metadata lost on current decode: EcN %d, EcM %d", decoded.EcN, decoded.EcM) + } +}