From 3d1245489ddaa87fcefce1b8e5e3d9ad592c8cd5 Mon Sep 17 00:00:00 2001 From: fuzzing-researcher Date: Sat, 8 Aug 2026 05:52:50 +0300 Subject: [PATCH] common/domain: avoid pre-allocating untrusted lengths in the set reader readUint64Slice and readByteSlice read a length via binary.ReadUvarint and then immediately allocate a slice of that size before reading the data. The length is untrusted (these readers parse serialized matcher data, reachable e.g. from sing-box .srs rule-sets via AdGuard domain items), so a crafted input can declare a huge length and OOM the process on the allocation, or panic with "makeslice: len out of range". Read incrementally instead: uint64 slices grow via append with a capped initial capacity; byte slices go through io.CopyN so the buffer grows only as bytes actually arrive. Add FuzzReadMatcher / FuzzReadAdGuardMatcher. --- common/domain/fuzz_test.go | 25 +++++++++++++++++++++++++ common/domain/set.go | 25 ++++++++++++++++++------- 2 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 common/domain/fuzz_test.go 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 {