diff --git a/common/domain/fuzz_test.go b/common/domain/fuzz_test.go new file mode 100644 index 00000000..8adc4dee --- /dev/null +++ b/common/domain/fuzz_test.go @@ -0,0 +1,25 @@ +package domain_test + +import ( + "bytes" + "testing" + + "github.com/sagernet/sing/common/domain" + "github.com/sagernet/sing/common/varbin" +) + +// FuzzReadMatcher / FuzzReadAdGuardMatcher fuzz the binary domain-matcher readers, which parse +// untrusted serialized data (e.g. reached from sing-box .srs rule-sets). They guard against +// panics and unbounded allocations when reading the succinct-set length fields. + +func FuzzReadMatcher(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + _, _ = domain.ReadMatcher(varbin.StubReader(bytes.NewReader(data))) + }) +} + +func FuzzReadAdGuardMatcher(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + _, _ = domain.ReadAdGuardMatcher(varbin.StubReader(bytes.NewReader(data))) + }) +} diff --git a/common/domain/set.go b/common/domain/set.go index 10b66bef..4a68b074 100644 --- a/common/domain/set.go +++ b/common/domain/set.go @@ -1,6 +1,7 @@ package domain import ( + "bytes" "encoding/binary" "io" "math/bits" @@ -122,10 +123,15 @@ func readUint64Slice(reader varbin.Reader) ([]uint64, error) { if length == 0 { return nil, nil } - result := make([]uint64, length) - err = binary.Read(reader, binary.BigEndian, result) - if err != nil { - return nil, err + // length is untrusted; grow via append so a crafted huge length hits EOF while reading + // the elements instead of OOMing the process on the allocation. + result := make([]uint64, 0, min(length, 64)) + for i := uint64(0); i < length; i++ { + var value uint64 + if err = binary.Read(reader, binary.BigEndian, &value); err != nil { + return nil, err + } + result = append(result, value) } return result, nil } @@ -149,12 +155,17 @@ func readByteSlice(reader varbin.Reader) ([]byte, error) { if length == 0 { return nil, nil } - result := make([]byte, length) - _, err = io.ReadFull(reader, result) + // length is untrusted; read through io.CopyN so the buffer grows only as bytes actually + // arrive, instead of pre-allocating make([]byte, length) and OOMing on a crafted length. + var buffer bytes.Buffer + _, err = io.CopyN(&buffer, reader, int64(length)) if err != nil { return nil, err } - return result, nil + if uint64(buffer.Len()) != length { + return nil, io.ErrUnexpectedEOF + } + return buffer.Bytes(), nil } func writeByteSlice(writer varbin.Writer, value []byte) error {