Files
seaweedfs/weed/storage/volume_write_test.go
T
hsdfatandChris Lu 06dda12e4b fix(volume): validate sizes in ReadNeedleBlob and WriteNeedleBlob (#11399)
* 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>
2026-09-19 21:28:37 -07:00

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")
}
}