diff --git a/.github/workflows/bench.yaml b/.github/workflows/bench.yaml index c60d630..a80fc1e 100644 --- a/.github/workflows/bench.yaml +++ b/.github/workflows/bench.yaml @@ -127,13 +127,13 @@ jobs: steps: - name: download baseline results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: baseline-results path: ./baseline - name: download head results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: head-results path: ./head diff --git a/.github/workflows/fuzz.yaml b/.github/workflows/fuzz.yaml new file mode 100644 index 0000000..d444523 --- /dev/null +++ b/.github/workflows/fuzz.yaml @@ -0,0 +1,103 @@ +name: fuzz + +on: + schedule: + - cron: '13 0 * * *' # every day at 00:13 UTC + + workflow_dispatch: + inputs: + fuzz_time: + description: '-fuzztime argument' + required: false + default: '5m' + fuzz_minimize_time: + description: '-fuzzminimizetime argument' + required: false + default: '60s' + +env: + # Default values that can be overridden + FUZZ_TIME: ${{ github.event.inputs.fuzz_time || '1h' }} + FUZZ_MINIMIZE_TIME: ${{ github.event.inputs.fuzz_minimize_time || '5m' }} + FUZZ_OUTPUT: ./fuzz.log + +permissions: + contents: write + pull-requests: write + +jobs: + fuzz_test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: stable + + - name: fuzz + id: fuzz + run: | + set -euo pipefail + mkdir -p /tmp/fuzz + + set +e + make fuzz | tee "${FUZZ_OUTPUT}" + FUZZ_EXIT_CODE=$? + + cat <> $GITHUB_STEP_SUMMARY + ### fuzz results + \`\`\` + $(cat "${FUZZ_OUTPUT}") + \`\`\` + EOF + + exit $FUZZ_EXIT_CODE + + - name: report failure + if: failure() + run: | + set -euo pipefail + + # make sure something actually changed before we open a PR + git add testdata/ + if git diff --staged --quiet; then + echo "No changes to testdata/ directory were found, assuming failure has already been reported." + exit 0 + fi + + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + BRANCH_NAME="fuzzer-failure/$(date +%Y%m%d-%H%M%S)" + git checkout -b $BRANCH_NAME + git commit -m "fuzz: add new findings" + git push origin $BRANCH_NAME + + cat < pr.txt + Scheduled fuzz testing found new failures: + + $(cat "${FUZZ_OUTPUT}") + + Next steps: + 1. Close and re-open this PR to trigger GitHub Actions (see [explanation here][1]) + 2. Examine the crash inputs in the `testdata/fuzz/` directory + 3. Run the fuzzer locally to reproduce: `make fuzz` + 4. ??? + 5. Profit! + + --- + *This PR was automatically created by the scheduled daily [fuzz action][2].* + + [1]: https://github.com/peter-evans/create-pull-request/blob/9b309f7eaa24cdc404c6e9e169d35ac06ca3671e/docs/concepts-guidelines.md#triggering-further-workflow-runs + [2]: ./.github/workflows/fuzz.yaml + EOF + + gh pr create \ + --base main \ + --head "${BRANCH_NAME}" \ + --title "fuzz: new failure on $(date +%Y/%m/%d)" \ + --body-file pr.txt \ + --label automated-pr \ + --label bug \ + --label fuzzing diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8dad91d..baaea5b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -103,10 +103,10 @@ jobs: fetch-depth: 0 # Full history for commit timestamps - name: setup pages - uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4 + uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4.0.0 - name: download latest autobahn report - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: autobahn-report path: "${{ env.OUT_DIR }}/latest-report" @@ -115,7 +115,7 @@ jobs: run: ./ci/build-github-pages - name: upload github pages artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: "${{ env.OUT_DIR }}/reports" diff --git a/Makefile b/Makefile index bc7fd87..7b8efa8 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ BENCH_COUNT ?= 10 BENCH_ARGS ?= -bench=. -benchmem -count=$(BENCH_COUNT) -run=^$$ DOCS_PORT ?= :8080 +FUZZ_TIME ?= 60s +FUZZ_MINIMIZE_TIME ?= 60s +FUZZ_ARGS ?= -fuzz=Fuzz -run=^Fuzz -fuzztime $(FUZZ_TIME) -fuzzminimizetime $(FUZZ_MINIMIZE_TIME) + # 3rd party tools CMD_GOFUMPT := go run mvdan.cc/gofumpt@v0.8.0 CMD_PKGSITE := go run golang.org/x/pkgsite/cmd/pkgsite@latest @@ -29,6 +33,11 @@ test: go test $(TEST_ARGS) ./... .PHONY: test +fuzz: + go test $(FUZZ_ARGS) . +.PHONY: fuzz + + # Test command to run for continuous integration, which includes code coverage # based on codecov.io's documentation: # https://github.com/codecov/example-go/blob/b85638743b972bd0bd2af63421fe513c6f968930/README.md diff --git a/proto_test.go b/proto_test.go index b302f31..c73d9c7 100644 --- a/proto_test.go +++ b/proto_test.go @@ -185,6 +185,50 @@ func TestIncompleteFrames(t *testing.T) { } } +// ============================================================================ +// Fuzzers +// ============================================================================ +func FuzzReadFrame(f *testing.F) { + // Set up seed corpus + var testCases [][]byte + // Example frames from RFC 6455 section 5.7 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.7 + testCases = append(testCases, [][]byte{ + // single-frame unmasked text + {0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}, + // single-frame masked text + {0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58}, + // fragmented unmasked text part 1 + {0x01, 0x03, 0x48, 0x65, 0x6c}, + // fragmented unmasked text part 2 + {0x80, 0x02, 0x6c, 0x6f}, + // unmasked ping + {0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}, + // masked ping response + {0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58}, + // 256 bytes binary message + append([]byte{0x82, 0x7E, 0x01, 0x00}, make([]byte, 256)...), + // 64KiB binary message + append([]byte{0x82, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}, make([]byte, 65536)...), + }...) + + for _, tc := range testCases { + f.Add(tc) + } + + f.Fuzz(func(t *testing.T, input []byte) { + modes := []websocket.Mode{websocket.ClientMode, websocket.ServerMode} + for _, mode := range modes { + frame, err := websocket.ReadFrame(bytes.NewReader(input), mode, 1<<20) + if err != nil { + t.Skipf("skipping eror: %s", err) + return + } + t.Logf("frame: %s", frame) + } + }) +} + // ============================================================================ // Benchmarks // ============================================================================