-
Notifications
You must be signed in to change notification settings - Fork 145
207 lines (187 loc) · 7.52 KB
/
run-fuzzer.yml
File metadata and controls
207 lines (187 loc) · 7.52 KB
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
name: Run Fuzzer
on:
workflow_call:
inputs:
fuzz_target:
description: "The cargo fuzz binary target name (e.g., file_io, array_ops)"
required: true
type: string
fuzz_name:
description: "Display/storage name for this fuzz run. Defaults to fuzz_target if not set."
required: false
type: string
default: ""
max_time:
description: "Maximum fuzzing time in seconds"
required: false
type: number
default: 2700
runner:
description: "Runner name from .github-private runs-on.yml (e.g., arm64-medium, gpu)"
required: false
type: string
default: "arm64-medium"
extra_features:
description: "Extra cargo features to enable (e.g., cuda)"
required: false
type: string
default: ""
extra_env:
description: "Extra environment variables to set (space-separated KEY=VALUE pairs)"
required: false
type: string
default: ""
jobs:
description: "Number of parallel fuzzing jobs (libfuzzer -fork=N). Set to match available vCPUs."
required: false
type: number
default: 1
outputs:
crashes_found:
description: "Whether crashes were found"
value: ${{ jobs.fuzz.outputs.crashes_found }}
first_crash_name:
description: "Name of the first crash file"
value: ${{ jobs.fuzz.outputs.first_crash_name }}
artifact_url:
description: "URL of the uploaded crash artifacts"
value: ${{ jobs.fuzz.outputs.artifact_url }}
secrets:
R2_FUZZ_ACCESS_KEY_ID:
required: true
R2_FUZZ_SECRET_ACCESS_KEY:
required: true
env:
NIGHTLY_TOOLCHAIN: nightly-2026-02-05
jobs:
fuzz:
name: "Run ${{ inputs.fuzz_name || inputs.fuzz_target }}"
env:
FUZZ_NAME: ${{ inputs.fuzz_name || inputs.fuzz_target }}
timeout-minutes: 240 # 4 hours
runs-on: >-
${{ github.repository == 'vortex-data/vortex'
&& format('runs-on={0}/runner={1}/disk=large/tag={2}-fuzz', github.run_id, inputs.runner, inputs.fuzz_name || inputs.fuzz_target)
|| 'ubuntu-latest' }}
outputs:
crashes_found: ${{ steps.check.outputs.crashes_found }}
first_crash_name: ${{ steps.check.outputs.first_crash_name }}
artifact_url: ${{ steps.upload_artifacts.outputs.artifact-url }}
steps:
- uses: runs-on/action@v2
if: github.repository == 'vortex-data/vortex'
with:
sccache: s3
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
toolchain: ${{ env.NIGHTLY_TOOLCHAIN }}
- name: Install llvm
uses: aminya/setup-cpp@v1
with:
compiler: llvm
- name: Install cargo-fuzz
uses: taiki-e/cache-cargo-install-action@66c9585ef5ca780ee69399975a5e911f47905995
with:
tool: cargo-fuzz
- name: Restore corpus
shell: bash
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }}
AWS_REGION: "us-east-1"
AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com"
run: |
CORPUS_KEY="${FUZZ_NAME}_corpus.tar.zst"
CORPUS_DIR="fuzz/corpus/${FUZZ_NAME}"
# Try to download corpus
if python3 scripts/s3-download.py "s3://vortex-fuzz-corpus/$CORPUS_KEY" "$CORPUS_KEY"; then
echo "Downloaded corpus successfully"
tar -xf "$CORPUS_KEY"
else
echo "Creating empty corpus directory"
mkdir -p "$CORPUS_DIR"
fi
- name: Run fuzzing target
id: fuzz
run: |
FEATURES_FLAG=""
if [ -n "${{ inputs.extra_features }}" ]; then
FEATURES_FLAG="--features ${{ inputs.extra_features }}"
fi
FORK_FLAG=""
if [ "${{ inputs.jobs }}" -gt 1 ]; then
FORK_FLAG="-fork=${{ inputs.jobs }}"
fi
${{ inputs.extra_env }} RUST_BACKTRACE=1 \
cargo +$NIGHTLY_TOOLCHAIN fuzz run --release --debug-assertions \
$FEATURES_FLAG \
${{ inputs.fuzz_target }} -- \
$FORK_FLAG -max_total_time=${{ inputs.max_time }} -rss_limit_mb=0 \
2>&1 | tee fuzz_output.log
continue-on-error: true
- name: Check for crashes
id: check
run: |
# Find actual crash files, not just the directory structure
FIRST_CRASH=$(find fuzz/artifacts -type f \( -name "crash-*" -o -name "leak-*" -o -name "timeout-*" -o -name "oom-*" \) 2>/dev/null | head -1 || true)
if [ -n "$FIRST_CRASH" ]; then
echo "crashes_found=true" >> $GITHUB_OUTPUT
echo "first_crash=$FIRST_CRASH" >> $GITHUB_OUTPUT
echo "first_crash_name=$(basename $FIRST_CRASH)" >> $GITHUB_OUTPUT
# Count all crashes for reporting
CRASH_COUNT=$(find fuzz/artifacts -type f \( -name "crash-*" -o -name "leak-*" -o -name "timeout-*" -o -name "oom-*" \) | wc -l)
echo "crash_count=$CRASH_COUNT" >> $GITHUB_OUTPUT
echo "Found $CRASH_COUNT crash(es), will process first: $(basename $FIRST_CRASH)"
else
echo "crashes_found=false" >> $GITHUB_OUTPUT
echo "crash_count=0" >> $GITHUB_OUTPUT
echo "No crashes found"
fi
- name: Reproduce crash for full output
if: steps.check.outputs.crashes_found == 'true' && inputs.jobs > 1
run: |
# In fork mode, child output (backtrace, panic message) is not captured in
# fuzz_output.log. Replay the crashing input in single-process mode to get
# the full output for the crash reporting pipeline.
FEATURES_FLAG=""
if [ -n "${{ inputs.extra_features }}" ]; then
FEATURES_FLAG="--features ${{ inputs.extra_features }}"
fi
RUST_BACKTRACE=1 \
cargo +$NIGHTLY_TOOLCHAIN fuzz run --release --debug-assertions \
$FEATURES_FLAG \
${{ inputs.fuzz_target }} \
"${{ steps.check.outputs.first_crash }}" \
2>&1 | tee fuzz_output.log || true
- name: Archive crash artifacts
id: upload_artifacts
if: steps.check.outputs.crashes_found == 'true'
uses: actions/upload-artifact@v7
with:
name: ${{ env.FUZZ_NAME }}-crash-artifacts
path: fuzz/artifacts
retention-days: 180
- name: Archive fuzzer output log
if: steps.check.outputs.crashes_found == 'true'
uses: actions/upload-artifact@v7
with:
name: ${{ env.FUZZ_NAME }}-logs
path: fuzz_output.log
retention-days: 90
- name: Persist corpus
shell: bash
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_FUZZ_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_FUZZ_SECRET_ACCESS_KEY }}
AWS_REGION: "us-east-1"
AWS_ENDPOINT_URL: "https://01e9655179bbec953276890b183039bc.r2.cloudflarestorage.com"
run: |
CORPUS_KEY="${FUZZ_NAME}_corpus.tar.zst"
CORPUS_DIR="fuzz/corpus/${FUZZ_NAME}"
tar -acf "$CORPUS_KEY" "$CORPUS_DIR"
python3 scripts/s3-upload.py --bucket vortex-fuzz-corpus --key "$CORPUS_KEY" --body "$CORPUS_KEY" --checksum-algorithm CRC32 --optimistic-lock
- name: Fail job if fuzz run found a bug
if: steps.check.outputs.crashes_found == 'true'
run: exit 1