mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-28 02:44:23 +00: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>
106 lines
3.6 KiB
Go
106 lines
3.6 KiB
Go
package needle
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
. "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
// readNeedleBodyBytes runs ReadNeedleBodyBytes and turns a panic into a test
|
|
// failure, so a regression reports which case broke instead of killing the run.
|
|
func readNeedleBodyBytes(t *testing.T, n *Needle, body []byte, version Version) (err error) {
|
|
t.Helper()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("ReadNeedleBodyBytes panicked for size %d, body length %d: %v", n.Size, len(body), r)
|
|
}
|
|
}()
|
|
return n.ReadNeedleBodyBytes(body, version)
|
|
}
|
|
|
|
// The size feeds the read buffer's length, so a negative one never reaches make().
|
|
func TestReadNeedleBlobRejectsNegativeSize(t *testing.T) {
|
|
for _, version := range []Version{Version1, Version2, Version3} {
|
|
for _, size := range []Size{TombstoneFileSize, -100} {
|
|
if _, err := ReadNeedleBlob(nil, 0, size, version); !errors.Is(err, ErrorSizeInvalid) {
|
|
t.Fatalf("version %d size %d: expected ErrorSizeInvalid, got %v", version, size, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A corrupted .dat header can carry a size that does not fit the body read for
|
|
// it. Vacuum used to panic on it with "slice bounds out of range [:-1]" (#6763).
|
|
func TestReadNeedleBodyBytesRejectsCorruptSize(t *testing.T) {
|
|
for version := Version1; IsSupportedVersion(version); version++ {
|
|
t.Run(versionString(version), func(t *testing.T) {
|
|
// A size of -1 is the case from #6763: its body length is still
|
|
// positive, so the scan reads a body and hands it over.
|
|
bodyLength := NeedleBodyLength(-1, version)
|
|
if bodyLength <= 0 {
|
|
t.Fatalf("expected a positive body length for size -1, got %d", bodyLength)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
size Size
|
|
body int
|
|
}{
|
|
{"size -1", -1, int(bodyLength)},
|
|
{"size -12", -12, 32},
|
|
{"size larger than body", 64, 32},
|
|
{"no room for the tail", 32, 32},
|
|
{"empty body", 0, 0},
|
|
{"empty body with data size", 1, 0},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
n := &Needle{Size: c.size}
|
|
err := readNeedleBodyBytes(t, n, make([]byte, c.body), version)
|
|
if !errors.Is(err, ErrorCorrupted) {
|
|
t.Fatalf("expected an error wrapping ErrorCorrupted, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The size guard must still accept every record the writer produces,
|
|
// including the size-0 record a delete appends.
|
|
func TestReadNeedleBodyBytesWrittenNeedles(t *testing.T) {
|
|
for version := Version1; IsSupportedVersion(version); version++ {
|
|
t.Run(versionString(version), func(t *testing.T) {
|
|
for _, data := range [][]byte{nil, []byte("hello seaweed")} {
|
|
written := &Needle{Id: 7, Cookie: 9, Data: data, Checksum: NewCRC(data), AppendAtNs: 42}
|
|
buf := new(bytes.Buffer)
|
|
if _, _, err := writeNeedleByVersion(version, written, 0, buf); err != nil {
|
|
// Some builds can read a version they cannot write; skip
|
|
// only that recognized case so a real writer regression
|
|
// still fails the test.
|
|
if strings.Contains(strings.ToLower(err.Error()), "unsupported version") {
|
|
t.Skipf("version %d is not writable in this build: %v", version, err)
|
|
}
|
|
t.Fatalf("write needle: %v", err)
|
|
}
|
|
|
|
n := new(Needle)
|
|
n.ParseNeedleHeader(buf.Bytes())
|
|
body := buf.Bytes()[NeedleHeaderSize:]
|
|
if int64(len(body)) != NeedleBodyLength(n.Size, version) {
|
|
t.Fatalf("body length %d, want %d", len(body), NeedleBodyLength(n.Size, version))
|
|
}
|
|
if err := readNeedleBodyBytes(t, n, body, version); err != nil {
|
|
t.Fatalf("read %d-byte needle: %v", len(data), err)
|
|
}
|
|
if !bytes.Equal(n.Data, data) {
|
|
t.Fatalf("data %q, want %q", n.Data, data)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|