mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* fix(volume): reject negative sizes in ReadNeedleBlob and WriteNeedleBlob A ReadNeedleBlob RPC with a size of -44 or below (-36 on v2 volumes) panics in makeslice inside needle.ReadNeedleBlob. The volume gRPC server has no recovery interceptor, so one request kills the process. Smaller negative sizes return bytes that are not a record. WriteNeedleBlob accepted a negative size whenever the blob header carried the same value: it appended the blob to .dat and indexed the needle with that size, which reads as deleted. Reject size < 0 in both Volume methods. Size 0 still passes, since delete records carry it. The Rust volume server got the same storage guards in #11345. * fix(volume): reject needle blobs whose length does not match their size WriteNeedleBlob appends the blob as is. A blob that is not the length its size implies leaves .dat off the 8-byte grid, and every later ordinary write to the volume is indexed at a truncated offset and reads back as EOF. A blob off by 8 bytes keeps the grid but leaves bytes that a .dat scan reads as the next record. The in-tree callers already send exact lengths. The one case this newly refuses is a copy between volumes of different needle versions, and that case already writes a broken record: a v3 record lands on a v2 volume with 8 extra bytes, and a v2 record on a v3 volume either fails the timestamp check or lands 8 bytes short. This is separate from the negative-size guards, whose Rust counterpart is #11345. The Rust server does not check the length yet. * fix(volume): guard the blob buffer allocation in needle.ReadNeedleBlob Volume.ReadNeedleBlob rejected negative sizes, but needle.ReadNeedleBlob still sized its buffer from the size and is called directly by vacuum and other paths. Reject a deletion marker before make() there too, and use size.IsDeleted() in the volume-level checks. * fix(volume): mirror the blob length check in the rust volume server write_needle_blob_and_index checked the size against the blob header but appended the blob verbatim, so a blob that is not the length its size implies still leaves .dat off the record grid. Match the Go check. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com>
450 lines
14 KiB
Go
450 lines
14 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestSearchVolumesWithDeletedNeedles(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()
|
|
|
|
count := 20
|
|
|
|
for i := 1; i < count; i++ {
|
|
n := newRandomNeedle(uint64(i))
|
|
_, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
for i := 1; i < 15; i++ {
|
|
n := newEmptyNeedle(uint64(i))
|
|
err := v.nm.Put(n.Id, types.Offset{}, types.TombstoneFileSize)
|
|
if err != nil {
|
|
t.Fatalf("delete needle %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
ts1 := time.Now().UnixNano()
|
|
|
|
for i := 15; i < count; i++ {
|
|
n := newEmptyNeedle(uint64(i))
|
|
_, err := v.doDeleteRequest(n)
|
|
if err != nil {
|
|
t.Fatalf("delete needle %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
offset, isLast, err := v.BinarySearchByAppendAtNs(uint64(ts1))
|
|
if err != nil {
|
|
t.Fatalf("lookup by ts: %v", err)
|
|
}
|
|
fmt.Printf("offset: %v, isLast: %v\n", offset.ToActualOffset(), isLast)
|
|
|
|
}
|
|
|
|
func isFileExist(path string) (bool, error) {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return true, nil
|
|
} else if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
} else {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
func assertFileExist(t *testing.T, expected bool, path string) {
|
|
exist, err := isFileExist(path)
|
|
if err != nil {
|
|
t.Fatalf("isFileExist: %v", err)
|
|
}
|
|
assert.Equal(t, expected, exist)
|
|
}
|
|
|
|
func TestDestroyEmptyVolumeWithOnlyEmpty(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)
|
|
}
|
|
path := v.DataBackend.Name()
|
|
|
|
// should can Destroy empty volume with onlyEmpty
|
|
assertFileExist(t, true, path)
|
|
err = v.Destroy(true, false)
|
|
if err != nil {
|
|
t.Fatalf("destroy volume: %v", err)
|
|
}
|
|
assertFileExist(t, false, path)
|
|
}
|
|
|
|
func TestDestroyEmptyVolumeWithoutOnlyEmpty(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)
|
|
}
|
|
path := v.DataBackend.Name()
|
|
|
|
// should can Destroy empty volume without onlyEmpty
|
|
assertFileExist(t, true, path)
|
|
err = v.Destroy(false, false)
|
|
if err != nil {
|
|
t.Fatalf("destroy volume: %v", err)
|
|
}
|
|
assertFileExist(t, false, path)
|
|
}
|
|
|
|
func TestDestroyNonemptyVolumeWithOnlyEmpty(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()
|
|
path := v.DataBackend.Name()
|
|
|
|
// should return "volume not empty" error and do not delete file when Destroy non-empty volume
|
|
_, _, _, err = v.writeNeedle2(newRandomNeedle(1), true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1), v.FileCount())
|
|
|
|
assertFileExist(t, true, path)
|
|
err = v.Destroy(true, false)
|
|
assert.EqualError(t, err, "volume not empty")
|
|
assertFileExist(t, true, path)
|
|
|
|
// should keep working after "volume not empty"
|
|
_, _, _, err = v.writeNeedle2(newRandomNeedle(2), true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
|
|
assert.Equal(t, uint64(2), v.FileCount())
|
|
}
|
|
|
|
func TestDestroyNonemptyVolumeWithoutOnlyEmpty(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)
|
|
}
|
|
path := v.DataBackend.Name()
|
|
|
|
// should can Destroy non-empty volume without onlyEmpty
|
|
_, _, _, err = v.writeNeedle2(newRandomNeedle(1), true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1), v.FileCount())
|
|
|
|
assertFileExist(t, true, path)
|
|
err = v.Destroy(false, false)
|
|
if err != nil {
|
|
t.Fatalf("destroy volume: %v", err)
|
|
}
|
|
assertFileExist(t, false, path)
|
|
}
|
|
|
|
// Pre-fix: the blob was appended to .dat, then rejected by SortedFileNeedleMap.Put.
|
|
func TestWriteNeedleBlobRejectedOnReadOnlyVolume(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
blob, err := v.ReadNeedleBlob(int64(offset), n.Size)
|
|
if err != nil {
|
|
t.Fatalf("read needle blob: %v", err)
|
|
}
|
|
if err := v.PersistReadOnly(true, false); err != nil {
|
|
t.Fatalf("persist read-only: %v", err)
|
|
}
|
|
v.Close()
|
|
|
|
v, err = NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume reload: %v", err)
|
|
}
|
|
defer v.Close()
|
|
if _, ok := v.nm.(*SortedFileNeedleMap); !ok {
|
|
t.Fatalf("reloaded read-only volume should use SortedFileNeedleMap, got %T", v.nm)
|
|
}
|
|
|
|
datSizeBefore, _, _ := v.DataBackend.GetStat()
|
|
|
|
err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), blob, n.Size)
|
|
if err == nil {
|
|
t.Fatalf("expected WriteNeedleBlob to be rejected on a read-only volume")
|
|
}
|
|
if errors.Is(err, os.ErrInvalid) {
|
|
t.Errorf("WriteNeedleBlob should fail with a read-only error, not the needle map's os.ErrInvalid: %v", err)
|
|
}
|
|
|
|
datSizeAfter, _, _ := v.DataBackend.GetStat()
|
|
if datSizeAfter != datSizeBefore {
|
|
t.Errorf("read-only volume .dat grew from %d to %d, leaving an unindexed needle", datSizeBefore, datSizeAfter)
|
|
}
|
|
}
|
|
|
|
// A size disagreeing with the blob's own header indexes the needle at the wrong
|
|
// length and, on v3, stamps the append timestamp into the middle of the needle.
|
|
func TestWriteNeedleBlobRejectsSizeMismatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil, stats.DefaultDiskIOProbeConfig())
|
|
defer location.Close()
|
|
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
location.SetVolume(7, v)
|
|
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
blob, err := v.ReadNeedleBlob(int64(offset), n.Size)
|
|
if err != nil {
|
|
t.Fatalf("read needle blob: %v", err)
|
|
}
|
|
|
|
datSizeBefore, _, _ := v.DataBackend.GetStat()
|
|
|
|
// types.Size(n.DataSize) is what needle.Append reports, and what a caller
|
|
// following the payload-size convention would send.
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), blob, types.Size(n.DataSize)); err == nil {
|
|
t.Fatal("expected WriteNeedleBlob to reject a size that disagrees with the blob header")
|
|
}
|
|
|
|
datSizeAfter, _, _ := v.DataBackend.GetStat()
|
|
if datSizeAfter != datSizeBefore {
|
|
t.Errorf(".dat grew from %d to %d on a rejected blob", datSizeBefore, datSizeAfter)
|
|
}
|
|
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), blob, n.Size); err != nil {
|
|
t.Fatalf("write needle blob with the header size: %v", err)
|
|
}
|
|
}
|
|
|
|
// A negative size reaches make() in needle.ReadNeedleBlob, and the blob RPCs
|
|
// have no recover, so one request took down the volume server.
|
|
func TestReadNeedleBlobRejectsNegativeSize(t *testing.T) {
|
|
dir := t.TempDir()
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
|
|
for _, size := range []types.Size{types.TombstoneFileSize, -100, math.MinInt32} {
|
|
t.Run(fmt.Sprintf("size %d", size), func(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("ReadNeedleBlob panicked: %v", r)
|
|
}
|
|
}()
|
|
if _, err := v.ReadNeedleBlob(int64(offset), size); err == nil {
|
|
t.Error("expected ReadNeedleBlob to reject a negative size")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Only negative sizes are rejected: size 0 is what a delete record carries.
|
|
for _, size := range []types.Size{0, n.Size} {
|
|
if _, err := v.ReadNeedleBlob(int64(offset), size); err != nil {
|
|
t.Errorf("ReadNeedleBlob with size %d: %v", size, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The blob header carries the same negative size, so only the sign is wrong.
|
|
func TestWriteNeedleBlobRejectsNegativeSize(t *testing.T) {
|
|
for _, size := range []types.Size{types.TombstoneFileSize, -5} {
|
|
t.Run(fmt.Sprintf("size %d", size), func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
blob, err := v.ReadNeedleBlob(int64(offset), n.Size)
|
|
if err != nil {
|
|
t.Fatalf("read needle blob: %v", err)
|
|
}
|
|
// Make the header agree with the size, so only the sign is wrong.
|
|
types.SizeToBytes(blob[types.NeedleHeaderSize-types.SizeSize:types.NeedleHeaderSize], size)
|
|
|
|
datSizeBefore, _, _ := v.DataBackend.GetStat()
|
|
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), blob, size); err == nil {
|
|
t.Error("expected WriteNeedleBlob to reject a negative size")
|
|
}
|
|
|
|
datSizeAfter, _, _ := v.DataBackend.GetStat()
|
|
if datSizeAfter != datSizeBefore {
|
|
t.Errorf(".dat grew from %d to %d on a rejected blob", datSizeBefore, datSizeAfter)
|
|
}
|
|
if nv, ok := v.nm.Get(types.Uint64ToNeedleId(2)); ok {
|
|
t.Errorf("needle 2 was indexed with size %d", nv.Size)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A blob shorter or longer than its record size leaves .dat off the record grid:
|
|
// later writes index at truncated offsets, or a scan reads the leftover bytes as
|
|
// the next record.
|
|
func TestWriteNeedleBlobRejectsLengthMismatch(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
mutate func(blob []byte) []byte
|
|
}{
|
|
{"one byte too long", func(blob []byte) []byte { return append(blob, 0) }},
|
|
{"one byte short", func(blob []byte) []byte { return blob[:len(blob)-1] }},
|
|
{"8 bytes too long", func(blob []byte) []byte { return append(blob, make([]byte, 8)...) }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
blob, err := v.ReadNeedleBlob(int64(offset), n.Size)
|
|
if err != nil {
|
|
t.Fatalf("read needle blob: %v", err)
|
|
}
|
|
datSizeBefore, _, _ := v.DataBackend.GetStat()
|
|
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), tc.mutate(blob), n.Size); err == nil {
|
|
t.Error("expected WriteNeedleBlob to reject a blob whose length does not match its size")
|
|
}
|
|
|
|
datSizeAfter, _, _ := v.DataBackend.GetStat()
|
|
if datSizeAfter != datSizeBefore {
|
|
t.Errorf(".dat grew from %d to %d on a rejected blob", datSizeBefore, datSizeAfter)
|
|
}
|
|
|
|
next := newRandomNeedle(3)
|
|
if _, _, _, err = v.writeNeedle2(next, true, false, false); err != nil {
|
|
t.Fatalf("write needle 3: %v", err)
|
|
}
|
|
got := newEmptyNeedle(3)
|
|
if _, err = v.readNeedle(got, nil, nil); err != nil {
|
|
t.Fatalf("read back needle 3: %v", err)
|
|
}
|
|
if !bytes.Equal(got.Data, next.Data) {
|
|
t.Error("needle 3 read back with different data")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The checks must pass what the real callers send: a needle's own record and the
|
|
// size-0 record a delete leaves.
|
|
func TestWriteNeedleBlobRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
v, err := NewVolume(dir, dir, "", 7, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
n := newRandomNeedle(1)
|
|
offset, _, _, err := v.writeNeedle2(n, true, false, false)
|
|
if err != nil {
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
blob, err := v.ReadNeedleBlob(int64(offset), n.Size)
|
|
if err != nil {
|
|
t.Fatalf("read needle blob: %v", err)
|
|
}
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(2), blob, n.Size); err != nil {
|
|
t.Fatalf("write needle blob: %v", err)
|
|
}
|
|
got := newEmptyNeedle(2)
|
|
if _, err = v.readNeedle(got, nil, nil); err != nil {
|
|
t.Fatalf("read back needle 2: %v", err)
|
|
}
|
|
if !bytes.Equal(got.Data, n.Data) {
|
|
t.Error("needle 2 read back with different data")
|
|
}
|
|
|
|
deleteOffset, _, _ := v.DataBackend.GetStat()
|
|
if _, err = v.doDeleteRequest(newEmptyNeedle(1)); err != nil {
|
|
t.Fatalf("delete needle 1: %v", err)
|
|
}
|
|
deleteRecord, err := v.ReadNeedleBlob(deleteOffset, 0)
|
|
if err != nil {
|
|
t.Fatalf("read delete record: %v", err)
|
|
}
|
|
if err = v.WriteNeedleBlob(types.Uint64ToNeedleId(3), deleteRecord, 0); err != nil {
|
|
t.Fatalf("write size-0 needle blob: %v", err)
|
|
}
|
|
|
|
next := newRandomNeedle(4)
|
|
if _, _, _, err = v.writeNeedle2(next, true, false, false); err != nil {
|
|
t.Fatalf("write needle 4: %v", err)
|
|
}
|
|
got = newEmptyNeedle(4)
|
|
if _, err = v.readNeedle(got, nil, nil); err != nil {
|
|
t.Fatalf("read back needle 4: %v", err)
|
|
}
|
|
if !bytes.Equal(got.Data, next.Data) {
|
|
t.Error("needle 4 read back with different data")
|
|
}
|
|
}
|