diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 00000000..1ed42cfe --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,5 @@ +FROM gcr.io/oss-fuzz-base/base-builder-go:v1 +RUN git clone --depth 1 https://github.com/containrrr/shoutrrr +COPY . $SRC/shoutrrr +WORKDIR $SRC/shoutrrr +COPY ./.clusterfuzzlite/build.sh $SRC/ diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100644 index 00000000..92c768ba --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,3 @@ +#!/binsh + +compile_go_fuzzer github.com/containrrr/shoutrrr/fuzz FuzzPartitionMessage fuzz_partition_message diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 00000000..eb93f278 --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: go \ No newline at end of file diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 00000000..27aad9cd --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,3 @@ +--- +exclude_paths: + - "fuzz/*" \ No newline at end of file diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 00000000..080859c1 --- /dev/null +++ b/.github/workflows/cflite_pr.yml @@ -0,0 +1,29 @@ +name: ClusterFuzzLite PR fuzzing +on: + workflow_dispatch: + pull_request: + paths: + - '**' +permissions: read-all +jobs: + PR: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sanitizer: [address] + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + sanitizer: ${{ matrix.sanitizer }} + language: go + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 400 + mode: 'code-change' + sanitizer: ${{ matrix.sanitizer }} diff --git a/fuzz/partition_message.go b/fuzz/partition_message.go new file mode 100644 index 00000000..f1ff62fa --- /dev/null +++ b/fuzz/partition_message.go @@ -0,0 +1,33 @@ +//go:build gofuzz +// +build gofuzz + +package fuzz + +import ( + fuzz "github.com/AdaLogics/go-fuzz-headers" + t "github.com/containrrr/shoutrrr/pkg/types" + "github.com/containrrr/shoutrrr/pkg/util" +) + +// FuzzPartitionMessage fuzzes the util.PartitionMessage function +func FuzzPartitionMessage(data []byte) int { + f := fuzz.NewConsumer(data) + + input, err := f.GetString() + if err != nil { + return 0 + } + + limits := t.MessageLimit{} + err = f.GenerateStruct(&limits) + if err != nil { + return 0 + } + + distance, err := f.GetInt() + if err != nil { + return 0 + } + _, _ = util.PartitionMessage(input, limits, distance) + return 1 +} diff --git a/pkg/types/message_limit.go b/pkg/types/message_limit.go index 238c441b..32b0a719 100644 --- a/pkg/types/message_limit.go +++ b/pkg/types/message_limit.go @@ -2,9 +2,9 @@ package types // MessageLimit is used for declaring the payload limits for services upstream APIs type MessageLimit struct { - ChunkSize int - TotalChunkSize int + ChunkSize uint + TotalChunkSize uint // Maximum number of chunks (including the last chunk for meta data) - ChunkCount int + ChunkCount uint } diff --git a/pkg/util/partition_message.go b/pkg/util/partition_message.go index 6f47dc23..acf81616 100644 --- a/pkg/util/partition_message.go +++ b/pkg/util/partition_message.go @@ -15,8 +15,8 @@ const ellipsis = " [...]" func PartitionMessage(input string, limits t.MessageLimit, distance int) (items []t.MessageItem, omitted int) { runes := []rune(input) chunkOffset := 0 - maxTotal := Min(len(runes), limits.TotalChunkSize) - maxCount := limits.ChunkCount - 1 + maxTotal := Min(len(runes), int(limits.TotalChunkSize)) + maxCount := int(limits.ChunkCount) - 1 if len(input) == 0 { // If the message is empty, return an empty array @@ -26,7 +26,7 @@ func PartitionMessage(input string, limits t.MessageLimit, distance int) (items for i := 0; i < maxCount; i++ { // If no suitable split point is found, use the chunkSize - chunkEnd := chunkOffset + limits.ChunkSize + chunkEnd := chunkOffset + int(limits.ChunkSize) // ... and start next chunk directly after this one nextChunkStart := chunkEnd if chunkEnd >= maxTotal { @@ -69,7 +69,7 @@ func Ellipsis(text string, maxLength int) string { // MessageItemsFromLines creates a set of MessageItems that is compatible with the supplied limits func MessageItemsFromLines(plain string, limits t.MessageLimit) (batches [][]t.MessageItem) { - maxCount := limits.ChunkCount + maxCount := int(limits.ChunkCount) lines := strings.Split(plain, "\n") batches = make([][]t.MessageItem, 0) @@ -78,9 +78,9 @@ func MessageItemsFromLines(plain string, limits t.MessageLimit) (batches [][]t.M totalLength := 0 for _, line := range lines { - maxLen := limits.ChunkSize + maxLen := int(limits.ChunkSize) - if len(items) == maxCount || totalLength+maxLen > limits.TotalChunkSize { + if len(items) == maxCount || totalLength+maxLen > int(limits.TotalChunkSize) { batches = append(batches, items) items = items[:0] } diff --git a/pkg/util/partition_message_test.go b/pkg/util/partition_message_test.go index 8f678aad..5ce1015e 100644 --- a/pkg/util/partition_message_test.go +++ b/pkg/util/partition_message_test.go @@ -67,14 +67,14 @@ var _ = Describe("Partition Message", func() { items, omitted := PartitionMessage(testString, unalignedLimits, 7) included := 0 for ii, item := range items { - expectedSize := unalignedLimits.ChunkSize + expectedSize := int(unalignedLimits.ChunkSize) // The last chunk might be smaller than the preceeding chunks if ii == len(items)-1 { // the chunk size is the remainder of, the total size, // or the max size, whatever is smallest, // and the previous chunk sizes - chunkSize := Min(inputLen, unalignedLimits.TotalChunkSize) % unalignedLimits.ChunkSize + chunkSize := Min(inputLen, int(unalignedLimits.TotalChunkSize)) % int(unalignedLimits.ChunkSize) // if the "rest" of the runes needs another chunk if chunkSize > 0 { // expect the chunk to contain the "rest" of the runes @@ -87,19 +87,19 @@ var _ = Describe("Partition Message", func() { if ii == len(items)-1 { for ri, r := range item.Text { runeOffset := (len(item.Text) - ri) - 1 - runeVal, err := strconv.ParseInt(string(r), 16, 64) - expectedLen := Min(inputLen, unalignedLimits.TotalChunkSize) + runeVal, err := strconv.ParseUint(string(r), 16, 64) + expectedLen := Min(inputLen, int(unalignedLimits.TotalChunkSize)) expectedVal := (expectedLen - runeOffset) % 16 Expect(err).ToNot(HaveOccurred()) - Expect(runeVal).To(Equal(int64(expectedVal))) + Expect(runeVal).To(BeEquivalentTo(expectedVal)) } } included += len(item.Text) - Expect(item.Text).To(HaveLen(expectedSize)) + Expect(item.Text).To(HaveLen(int(expectedSize))) } - Expect(omitted + included).To(Equal(inputLen)) + Expect(omitted + included).To(BeEquivalentTo(inputLen)) } }) @@ -133,8 +133,8 @@ var _ = Describe("Partition Message", func() { batches := testMessageItemsFromLines(hundreds, limits, repeat) items := batches[0] - Expect(len(items[0].Text)).To(Equal(limits.ChunkSize)) - Expect(len(items[1].Text)).To(Equal(limits.ChunkSize)) + Expect(len(items[0].Text)).To(BeEquivalentTo(limits.ChunkSize)) + Expect(len(items[1].Text)).To(BeEquivalentTo(limits.ChunkSize)) }) }) }) @@ -170,10 +170,10 @@ func testPartitionMessage(hundreds int, limits types.MessageLimit, distance int) items, omitted = PartitionMessage(builder.String(), limits, distance) - contentSize := Min(hundreds*100, limits.TotalChunkSize) + contentSize := Min(hundreds*100, int(limits.TotalChunkSize)) expectedOmitted := Max(0, (hundreds*100)-contentSize) - ExpectWithOffset(0, omitted).To(Equal(expectedOmitted)) + ExpectWithOffset(0, omitted).To(BeEquivalentTo(expectedOmitted)) return }