-
Notifications
You must be signed in to change notification settings - Fork 25
Fuzzing: add libFuzzer targets for QUIC packet and transport parameter parsing #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rpaulo
wants to merge
1
commit into
apple:main
Choose a base branch
from
rpaulo:fuzzer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| name: Nightly fuzzing | ||
|
|
||
| on: | ||
| schedule: | ||
| # 03:00 UTC daily. | ||
| - cron: '0 3 * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| durations: | ||
| description: 'Per-target fuzz durations, as <target>=<seconds> pairs' | ||
| required: false | ||
| default: 'FuzzQUICPackets=2400 FuzzTransportParameters=600' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| fuzz: | ||
| name: Fuzz | ||
| uses: ./.github/workflows/fuzz.yml | ||
| with: | ||
| durations: ${{ inputs.durations || 'FuzzQUICPackets=2400 FuzzTransportParameters=600' }} | ||
| persist_corpus: true | ||
| timeout_minutes: 120 | ||
| crash_retention_days: 90 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| name: Fuzz | ||
|
|
||
| # Reusable fuzzing job, called by pull_request.yml (short smoke run) and | ||
| # fuzz-nightly.yml (long run with a persisted corpus). The inputs cover | ||
| # everything that differs between the two, so the build/run/upload steps only | ||
| # exist here. | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| durations: | ||
| description: 'Per-target fuzz durations, as <target>=<seconds> pairs' | ||
| required: true | ||
| type: string | ||
| persist_corpus: | ||
| description: 'Carry the corpus across runs via actions/cache, so coverage accumulates' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
| timeout_minutes: | ||
| description: 'Job timeout in minutes; must comfortably exceed the sum of the durations' | ||
| required: false | ||
| default: 30 | ||
| type: number | ||
| crash_retention_days: | ||
| description: 'How long to keep uploaded crash reproducers' | ||
| required: false | ||
| default: 14 | ||
| type: number | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| fuzz: | ||
| name: Fuzzing | ||
| runs-on: ubuntu-latest | ||
| container: | ||
| image: swift:6.3 | ||
| timeout-minutes: ${{ inputs.timeout_minutes }} | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v7 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Swift version | ||
| run: swift --version | ||
|
|
||
| # No exact-key hit on a scheduled run (the key includes github.run_id, | ||
| # always new), so this falls through to restore-keys and picks up the most | ||
| # recent previous corpus. First run, or after eviction, is simply a no-op | ||
| # and the fuzzers start from empty. | ||
| - name: Restore fuzz corpus cache | ||
| if: inputs.persist_corpus | ||
| uses: actions/cache/restore@v4 | ||
| with: | ||
| path: fuzz-corpus | ||
| key: fuzz-corpus-${{ github.run_id }} | ||
| restore-keys: | | ||
| fuzz-corpus- | ||
|
|
||
| - name: Build fuzz targets (release) | ||
| # Build *only* the two fuzz products. The Fuzzing trait applies the | ||
| # sanitizer flags package-wide, and linking the instrumented | ||
| # command-line tools trips an ld.gold bug ("internal error in | ||
| # format_file_lineno, at ../../gold/dwarf_reader.cc"). The fuzzers | ||
| # themselves link fine, and the tools aren't needed here anyway. | ||
| # | ||
| # DisableDebugLogging/DisableErrorLogging keep the library quiet: every | ||
| # rejected input otherwise logs, which buries the libFuzzer output and | ||
| # costs throughput. Crash diagnosis doesn't need it - the stack trace | ||
| # comes from libFuzzer/ASan, and a saved reproducer can be replayed | ||
| # locally with logging left on. | ||
| run: | | ||
| for product in FuzzQUICPackets FuzzTransportParameters; do | ||
| swift build --configuration release --traits Fuzzing,DisableDebugLogging,DisableErrorLogging --product "$product" | ||
| done | ||
|
|
||
| - name: Fuzz | ||
| # Unquoted on purpose: the spec is several space-separated arguments. | ||
| run: bash .github/workflows/scripts/ci-fuzz.sh ${{ inputs.durations }} | ||
|
|
||
| # Runs even though the Fuzz step failed (i.e. found a crash/oom/timeout), | ||
| # so the failing input can be downloaded and replayed locally. | ||
| - name: Upload crash artifacts | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: fuzz-crashes-${{ github.run_id }} | ||
| path: | | ||
| fuzz-corpus/*/crash-* | ||
| fuzz-corpus/*/oom-* | ||
| fuzz-corpus/*/timeout-* | ||
| if-no-files-found: ignore | ||
| retention-days: ${{ inputs.crash_retention_days }} | ||
|
|
||
| # Always re-save, even on failure, so the next run keeps this run's | ||
| # coverage gains and the crashing input stays in the corpus as a | ||
| # regression check once it's fixed. | ||
| - name: Save fuzz corpus cache | ||
| if: always() && inputs.persist_corpus | ||
| uses: actions/cache/save@v4 | ||
| with: | ||
| path: fuzz-corpus | ||
| key: fuzz-corpus-${{ github.run_id }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #!/bin/bash | ||
| ##===----------------------------------------------------------------------===## | ||
| ## | ||
| ## This source file is part of the Swift open source project | ||
| ## | ||
| ## Copyright (c) 2026 Apple Inc. and the Swift project authors | ||
| ## Licensed under Apache License v2.0 | ||
| ## | ||
| ## See LICENSE.txt for license information | ||
| ## See CONTRIBUTORS.txt for the list of Swift project authors | ||
| ## | ||
| ## SPDX-License-Identifier: Apache-2.0 | ||
| ## | ||
| ##===----------------------------------------------------------------------===## | ||
|
|
||
| # Runs the fuzz targets, each for its own duration. Shared by two workflows: | ||
| # fuzz-nightly.yml calls this with long durations and persists the corpus across | ||
| # runs via actions/cache, for deep, cumulative fuzzing; pull_request.yml's | ||
| # fuzz-smoke job calls this with short durations and no persisted corpus, as a | ||
| # quick per-PR smoke check that the fuzz targets still build and don't crash | ||
| # immediately. | ||
| # | ||
| # Durations are per target rather than shared because the targets cover very | ||
| # different amounts of code: FuzzQUICPackets reaches the whole connection and | ||
| # frame-processing machinery and keeps finding coverage for a long time, while | ||
| # FuzzTransportParameters exercises one deserializer and saturates quickly, so | ||
| # extra time there buys little. | ||
| # | ||
| # Each target gets its own corpus subdirectory under FUZZ_CORPUS_DIR, which the | ||
| # calling workflow persists across runs (e.g. via actions/cache) so coverage | ||
| # accumulates day over day instead of restarting from empty every time. | ||
| # libFuzzer writes any crash-*/oom-*/timeout-* artifact directly into that same | ||
| # subdirectory (via -artifact_prefix), so the workflow can find and upload them | ||
| # after this script exits, regardless of whether it exits 0 or non-zero. | ||
| # | ||
| # Every target always runs, even if an earlier one finds a crash - this script | ||
| # only reports the overall failure (via ci_finish's exit code) once they have | ||
| # all had a chance to run, matching ci-linux.sh's "run every step" philosophy. | ||
| # | ||
| # Usage: ci-fuzz.sh <target>=<seconds> [<target>=<seconds> ...] | ||
| # | ||
| # e.g. ci-fuzz.sh FuzzQUICPackets=120 FuzzTransportParameters=30 | ||
| # | ||
| # Env: | ||
| # FUZZ_CORPUS_DIR - where the persisted corpus/crash artifacts live | ||
| # (default: fuzz-corpus) | ||
|
|
||
| set -u | ||
|
|
||
| script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| # shellcheck source=ci-support.sh | ||
| . "${script_dir}/ci-support.sh" | ||
|
|
||
| corpus_root="${FUZZ_CORPUS_DIR:-fuzz-corpus}" | ||
|
|
||
| usage() { | ||
| echo "usage: $(basename "$0") <target>=<seconds> [<target>=<seconds> ...]" >&2 | ||
| echo " e.g. $(basename "$0") FuzzQUICPackets=120 FuzzTransportParameters=30" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| fuzz_target() { | ||
| local binary="$1" | ||
| local seconds="$2" | ||
| local corpus_dir="${corpus_root}/${binary}" | ||
| mkdir -p "${corpus_dir}" | ||
| ci_run "Fuzz ${binary} (${seconds}s)" \ | ||
| ".build/release/${binary}" \ | ||
| -max_total_time="${seconds}" \ | ||
| -artifact_prefix="${corpus_dir}/" \ | ||
| "${corpus_dir}" | ||
| } | ||
|
|
||
| [ "$#" -gt 0 ] || usage | ||
|
|
||
| # Validate everything up front, so a typo fails immediately instead of after | ||
| # the first target has already fuzzed for several minutes. | ||
| for spec in "$@"; do | ||
| case "${spec}" in | ||
| *=*) ;; | ||
| *) echo "error: expected <target>=<seconds>, got '${spec}'" >&2; usage ;; | ||
| esac | ||
| binary="${spec%%=*}" | ||
| seconds="${spec##*=}" | ||
| case "${seconds}" in | ||
| '' | *[!0-9]*) | ||
| echo "error: '${seconds}' is not a whole number of seconds, in '${spec}'" >&2 | ||
| usage | ||
| ;; | ||
| esac | ||
| if [ ! -x ".build/release/${binary}" ]; then | ||
| echo "error: .build/release/${binary} is missing or not executable" >&2 | ||
| exit 2 | ||
| fi | ||
| done | ||
|
|
||
| for spec in "$@"; do | ||
| fuzz_target "${spec%%=*}" "${spec##*=}" | ||
| done | ||
|
|
||
| ci_finish |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this PR was opened I made a
main.ymljob which runs daily against main, I think this might make sense in there? But on the other hand it would then be rolled in to a top level status, your choice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If fuzzing fails, it will be clear that it's the "Fuzzing" job that failed, not the "Main" job, that's why I think they should be separate. Also, I prefer to work with smaller files... :)