Files
seaweedfs/weed/storage/volume_read_test.go
hsdfat 110b485bae fix(volume): stop ScanVolumeFileFrom at a header it cannot advance past (#11398)
fix(volume): stop scans at a header they cannot advance past

A corrupt .dat header with a very negative size gives a record length
(NeedleHeaderSize + NeedleBodyLength) of zero or less: v3 sizes -43..-36
and v2 sizes -35..-28 give exactly zero, and smaller sizes give a
negative length. ScanVolumeFileFrom advanced by that length, so it
re-read the same header forever or stepped back into the record before
it. weed fix, weed export, weed compact, incremental weed backup and the
tail sender behind volume.move and volume.merge could hang on such a
volume, and weed compact could also finish with a .cpx that had dropped
every needle after the header.

Return an error wrapping needle.ErrorCorrupted instead. The check runs
after the visitor has seen the record, so the rebuild scanner still
stops quietly with io.EOF. Smaller negative sizes whose record length is
positive are still stepped over, preserving the salvage behavior
compaction relies on.

Mirror the guard into the Rust volume scans: DatScanPlan::scan and
read_all_needles fail on a non-positive record length, as does
scan_dat_head, so a corrupt header cannot stall a tail pass or leave the
repair scan walking stale offsets.
2026-09-19 22:26:56 -07:00

245 lines
7.7 KiB
Go

package storage
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/stretchr/testify/assert"
)
func TestReadNeedleNilNeedleMap(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
defer v.Close()
v.dataFileAccessLock.Lock()
if v.nm != nil {
v.nm.Close()
v.nm = nil
}
v.dataFileAccessLock.Unlock()
n := new(needle.Needle)
n.Id = types.Uint64ToNeedleId(1)
if _, err := v.readNeedle(n, &ReadOption{}, nil); err != ErrorNotFound {
t.Fatalf("readNeedle: want ErrorNotFound, got %v", err)
}
err = v.readNeedleDataInto(n, &ReadOption{ReadBufferSize: 1024}, &bytes.Buffer{}, 0, 0)
if err != ErrorNotFound {
t.Fatalf("readNeedleDataInto: want ErrorNotFound, got %v", err)
}
}
func TestReadNeedMetaWithWritesAndUpdates(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
defer v.Close()
type WriteInfo struct {
offset int64
size int32
}
writeInfos := make([]WriteInfo, 30)
mockLastUpdateTime := uint64(1000000000000)
// initialize 20 needles then update first 10 needles
for i := 1; i <= 30; i++ {
n := newRandomNeedle(uint64(i % 20))
n.Flags = 0x08
n.LastModified = mockLastUpdateTime
mockLastUpdateTime += 2000
offset, _, _, err := v.writeNeedle2(n, true, false, false)
if err != nil {
t.Fatalf("write needle %d: %v", i, err)
}
writeInfos[i-1] = WriteInfo{offset: int64(offset), size: int32(n.Size)}
}
expectedLastUpdateTime := uint64(1000000000000)
for i := 0; i < 30; i++ {
testNeedle := new(needle.Needle)
testNeedle.Id = types.Uint64ToNeedleId(uint64(i + 1%20))
testNeedle.Flags = 0x08
v.readNeedleMetaAt(testNeedle, writeInfos[i].offset, writeInfos[i].size)
actualLastModifiedTime := testNeedle.LastModified
if writeInfos[i].size != 0 {
assert.Equal(t, expectedLastUpdateTime, actualLastModifiedTime, "The two words should be the same.")
}
expectedLastUpdateTime += 2000
}
}
func TestReadNeedMetaWithDeletesThenWrites(t *testing.T) {
dir := t.TempDir()
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
if err != nil {
t.Fatalf("volume creation: %v", err)
}
defer v.Close()
type WriteInfo struct {
offset int64
size int32
}
writeInfos := make([]WriteInfo, 10)
mockLastUpdateTime := uint64(1000000000000)
for i := 1; i <= 10; i++ {
n := newRandomNeedle(uint64(i % 5))
n.Flags = 0x08
n.LastModified = mockLastUpdateTime
mockLastUpdateTime += 2000
offset, _, _, err := v.writeNeedle2(n, true, false, false)
if err != nil {
t.Fatalf("write needle %d: %v", i, err)
}
if i < 5 {
size, err := v.deleteNeedle2(n)
if err != nil {
t.Fatalf("delete needle %d: %v", i, err)
}
writeInfos[i-1] = WriteInfo{offset: int64(offset), size: int32(size)}
} else {
writeInfos[i-1] = WriteInfo{offset: int64(offset), size: int32(n.Size)}
}
}
expectedLastUpdateTime := uint64(1000000000000)
for i := 0; i < 10; i++ {
testNeedle := new(needle.Needle)
testNeedle.Id = types.Uint64ToNeedleId(uint64(i + 1%5))
testNeedle.Flags = 0x08
v.readNeedleMetaAt(testNeedle, writeInfos[i].offset, writeInfos[i].size)
actualLastModifiedTime := testNeedle.LastModified
if writeInfos[i].size != 0 {
assert.Equal(t, expectedLastUpdateTime, actualLastModifiedTime, "The two words should be the same.")
}
expectedLastUpdateTime += 2000
}
}
// scanRecorder records visited offsets and fails once a scan visits more
// records than the file holds.
type scanRecorder struct {
readBody bool
maxVisits int
offsets []int64
}
func (s *scanRecorder) VisitSuperBlock(super_block.SuperBlock) error { return nil }
func (s *scanRecorder) ReadNeedleBody() bool { return s.readBody }
func (s *scanRecorder) VisitNeedle(_ *needle.Needle, offset int64, _, _ []byte) error {
s.offsets = append(s.offsets, offset)
if len(s.offsets) > s.maxVisits {
return fmt.Errorf("visited %d records in a file of %d, offsets %v", len(s.offsets), s.maxVisits, s.offsets)
}
return nil
}
var errDidNotReturn = errors.New("did not return in time")
// runWithTimeout returns errDidNotReturn if fn does not finish within d.
func runWithTimeout(d time.Duration, fn func() error) error {
done := make(chan error, 1)
go func() { done <- fn() }()
select {
case err := <-done:
return err
case <-time.After(d):
return errDidNotReturn
}
}
// A corrupt .dat header can make a record's length zero or negative; the scan
// must stop there with ErrorCorrupted rather than re-read the header or step
// back into the previous record. A negative size whose record length stays
// positive is stepped over as before.
func TestScanVolumeFileFrom_StopsAtRecordThatCannotAdvance(t *testing.T) {
cases := []struct {
version needle.Version
size types.Size
stops bool
}{
{needle.Version3, -1, false}, // record length 32
{needle.Version3, -36, true}, // record length 0
{needle.Version3, -43, true}, // record length 0
{needle.Version3, -44, true}, // record length -8
{needle.Version3, -4096, true}, // record length -4056
{needle.Version2, -1, false}, // record length 24
{needle.Version2, -28, true}, // record length 0
{needle.Version2, -35, true}, // record length 0
{needle.Version2, -36, true}, // record length -8
}
for _, tc := range cases {
recordLen := needle.GetActualSize(tc.size, tc.version)
if (recordLen <= 0) != tc.stops {
t.Fatalf("v%d size %d: record length %d, case expects stops=%v", tc.version, tc.size, recordLen, tc.stops)
}
for _, readBody := range []bool{false, true} {
t.Run(fmt.Sprintf("v%d/size%d/readBody=%v", tc.version, tc.size, readBody), func(t *testing.T) {
f, err := os.Create(filepath.Join(t.TempDir(), "1.dat"))
if err != nil {
t.Fatalf("create dat: %v", err)
}
dat := backend.NewDiskFile(f)
defer dat.Close()
first, _, _, err := newRandomNeedle(1).Append(dat, tc.version)
if err != nil {
t.Fatalf("append needle 1: %v", err)
}
corruptAt, _, err := dat.GetStat()
if err != nil {
t.Fatalf("stat dat: %v", err)
}
raw := make([]byte, max(recordLen, types.NeedleHeaderSize))
types.NeedleIdToBytes(raw[types.CookieSize:types.CookieSize+types.NeedleIdSize], types.Uint64ToNeedleId(99))
types.SizeToBytes(raw[types.CookieSize+types.NeedleIdSize:types.NeedleHeaderSize], tc.size)
if _, err := dat.WriteAt(raw, corruptAt); err != nil {
t.Fatalf("append corrupt record: %v", err)
}
second, _, _, err := newRandomNeedle(2).Append(dat, tc.version)
if err != nil {
t.Fatalf("append needle 2: %v", err)
}
scanner := &scanRecorder{readBody: readBody, maxVisits: 3}
err = runWithTimeout(10*time.Second, func() error {
return ScanVolumeFileFrom(tc.version, dat, 0, scanner)
})
want := []int64{int64(first), corruptAt, int64(second)}
if tc.stops {
want = want[:2]
if !errors.Is(err, needle.ErrorCorrupted) {
t.Errorf("scan error = %v, want one wrapping ErrorCorrupted", err)
}
} else if err != nil {
t.Errorf("scan error = %v, want nil", err)
}
if !reflect.DeepEqual(scanner.offsets, want) {
t.Errorf("visited offsets %v, want %v", scanner.offsets, want)
}
})
}
}
}