diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 000000000..02e0d0704 --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,43 @@ +name: CIFuzz +on: + pull_request: + paths: + - '**.go' + - '.github/workflows/cifuzz.yml' + push: + branches: [main, master] + +permissions: + contents: read + security-events: write + +jobs: + fuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sanitizer: [address] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@ba0e2e0399a10b7b42afb16e7a6c4ccd3ff52431 + with: + oss-fuzz-project-name: 'protobuf-go' + language: go + sanitizer: ${{ matrix.sanitizer }} + - name: Run Fuzzers (${{ matrix.sanitizer }}) + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@ba0e2e0399a10b7b42afb16e7a6c4ccd3ff52431 + with: + oss-fuzz-project-name: 'protobuf-go' + language: go + fuzz-seconds: 300 + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true + - name: Upload Sarif + if: always() && steps.build.outcome == 'success' + uses: github/codeql-action/upload-sarif@601d5b1bcb3e5ef5eea97a6d0dcdbbb8c2b80116 + with: + sarif_file: cifuzz-sarif/results.sarif + category: fuzz-${{ matrix.sanitizer }} diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 000000000..e961a9a20 --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,124 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protowire_test + +import ( + "testing" + + "google.golang.org/protobuf/encoding/protowire" +) + +// FuzzConsumeTag tests protobuf wire-format tag parsing with arbitrary bytes. +// Every protobuf message uses this wire format. A parsing bug here affects +// EVERY Go service using protobuf. +func FuzzConsumeTag(f *testing.F) { + f.Add([]byte{0x08, 0x01}) + f.Add([]byte{0x12, 0x07, 't', 'e', 's', 't', 'i', 'n', 'g'}) + f.Add([]byte{}) + f.Add([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01}) + + f.Fuzz(func(t *testing.T, data []byte) { + for len(data) > 0 { + _, _, n := protowire.ConsumeTag(data) + if n < 0 { + break + } + data = data[n:] + } + + data2 := make([]byte, len(data)) + copy(data2, data) + for len(data2) > 0 { + _, n := protowire.ConsumeVarint(data2) + if n < 0 { + break + } + data2 = data2[n:] + } + }) +} + +// FuzzWireRoundTrip tests protobuf wire encoding → decoding consistency. +func FuzzWireRoundTrip(f *testing.F) { + f.Add(uint32(1), uint32(0)) + f.Add(uint32(2), uint32(2)) + f.Add(uint32(5), uint32(5)) + + f.Fuzz(func(t *testing.T, fieldNum, wireType uint32) { + num := protowire.Number(1 + (fieldNum % ((1 << 29) - 1))) + typ := protowire.Type(wireType % 7) + + var buf []byte + buf = protowire.AppendTag(buf, num, typ) + + decodedNum, decodedType, n := protowire.ConsumeTag(buf) + if n < 0 { + t.Errorf("failed to decode tag: num=%d typ=%d buf=%x", num, typ, buf) + return + } + + if decodedNum != num || decodedType != typ { + t.Errorf("round-trip mismatch: (%d,%d) → (%d,%d)", num, typ, decodedNum, decodedType) + } + }) +} + +// FuzzVarintRoundTrip tests varint encoding → decoding. +func FuzzVarintRoundTrip(f *testing.F) { + f.Add(uint64(0)) + f.Add(uint64(1)) + f.Add(uint64(128)) + f.Add(uint64(1<<64 - 1)) + + f.Fuzz(func(t *testing.T, val uint64) { + buf := protowire.AppendVarint(nil, val) + + decoded, n := protowire.ConsumeVarint(buf) + if n < 0 { + t.Errorf("failed to decode varint: val=%d buf=%x", val, buf) + return + } + + if decoded != val { + t.Errorf("varint round-trip mismatch: %d → %x → %d", val, buf, decoded) + } + }) +} + +// FuzzConsumeField tests field consumption combining tag + value parsing. +func FuzzConsumeField(f *testing.F) { + f.Add([]byte{0x08, 0x2a}) + f.Add([]byte{0x12, 0x03, 0x61, 0x62, 0x63}) + f.Add([]byte{0x1a, 0x04, 0x00, 0x00, 0x00, 0x00}) + + f.Fuzz(func(t *testing.T, data []byte) { + for len(data) > 0 { + num, typ, tagLen := protowire.ConsumeTag(data) + if tagLen < 0 { + break + } + data = data[tagLen:] + + valLen := protowire.ConsumeFieldValue(num, typ, data) + if valLen < 0 { + break + } + data = data[valLen:] + } + }) +}