1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
name: Fuzz (nightly)
# Scheduled coverage-guided fuzzing. Finds panics, infinite loops, and
# memory issues in the parser that property tests and unit tests miss.
#
# Policy:
# - Runs nightly + on demand (workflow_dispatch); not a PR gate.
# - A crash FAILS the job so the red mark surfaces on the Actions
# dashboard. fuzz/artifacts/* uploads so the reproducer survives.
# - A scheduled failure files (or comments on) a tracking issue so it
# cannot pass unnoticed. Triage is manual. Reproducers get copied
# into tests/ as regressions — see docs/contributing/fuzzing.md.
# - The grown corpus is cached per target across runs so coverage
# depth compounds instead of regrowing from the committed seeds.
on:
schedule:
# Daily at 03:00 UTC. Offset from memory-safety.yml (02:00) so the
# two nightly jobs do not contend for the same cache slot.
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
max_total_time:
description: 'Seconds to fuzz (default 300 = 5 min)'
required: false
default: '300'
permissions:
contents: read
env:
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: ${{ matrix.target }} (${{ github.event.inputs.max_total_time || '300' }}s)
runs-on: ubuntu-latest
# Generous cap: toolchain install + cargo-fuzz install + build + 5-min
# fuzz + artifact upload. Raise if workflow_dispatch runs use longer
# budgets.
timeout-minutes: 20
strategy:
# Each target is independent — let one finding surface without
# cancelling the other.
fail-fast: false
matrix:
target:
steps:
- uses: actions/checkout@v6.0.3
- name: Install nightly Rust toolchain
uses: dtolnay/rust-toolchain@nightly
- name: Install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Cache cargo + fuzz target
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
fuzz/target
key: ${{ runner.os }}-cargo-fuzz-${{ hashFiles('fuzz/Cargo.toml', 'Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-fuzz-
- name: Cache fuzz corpus
uses: actions/cache@v5
with:
path: fuzz/corpus/${{ matrix.target }}
# The run-scoped key never matches an existing entry, so every
# run restores the newest corpus via restore-keys and saves its
# own grown corpus as a fresh entry. The committed seeds merge
# in from checkout either way.
key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
restore-keys: |
fuzz-corpus-${{ matrix.target }}-
- name: Run ${{ matrix.target }}
env:
MAX_TOTAL_TIME: ${{ github.event.inputs.max_total_time || '300' }}
# Run from the repo root — cargo-fuzz resolves `./fuzz/Cargo.toml`
# from cwd. `+nightly` overrides the root rust-toolchain.toml pin
# (1.95.0) with the nightly installed above, which libfuzzer-sys
# requires. `--target x86_64-unknown-linux-gnu` overrides cargo-fuzz's
# default musl target, which isn't installed on the runner and is
# incompatible with `-Zsanitizer=address` (static libc).
run: cargo +nightly fuzz run ${{ matrix.target }} --target x86_64-unknown-linux-gnu -- -max_total_time=$MAX_TOTAL_TIME
- name: Minimize corpus before caching
# Sheds redundant inputs while keeping coverage, so the cached
# corpus stays small enough for fast libfuzzer startup. Skipped
# when the fuzz run failed (the cache also skips saving then).
run: cargo +nightly fuzz cmin ${{ matrix.target }} --target x86_64-unknown-linux-gnu
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: fuzz-artifacts-${{ matrix.target }}
path: fuzz/artifacts/
if-no-files-found: warn
retention-days: 30
notify-failure:
name: File failure issue
needs: fuzz
if: (failure() || cancelled()) && github.event_name == 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
steps:
- name: File or update the failure-tracking issue
env:
GH_TOKEN: ${{ github.token }}
TITLE: "Nightly fuzz run failed"
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--json number,title \
--jq '[.[] | select(.title == env.TITLE)][0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "$GITHUB_REPOSITORY" \
--body "Failed again: $RUN_URL"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" \
--body "Scheduled run failed: $RUN_URL — crash artifacts (if any) are attached to the run. Later failures will be added here as comments. Triage playbook: docs/contributing/fuzzing.md."
fi