Skip to content

Commit e0538c9

Browse files
martinusclaude
andcommitted
Actually fuzz on a schedule, instead of only replaying the corpus
The 1651 inputs in data/fuzz/api are replayed by the test suite on every run, which is a regression guard and by construction can only find what has already been found. Nothing was doing the finding. fuzz_api is even built in CI already, by the sanitizers job, because it is gated on the compiler being clang -- it was just never run as a fuzzer. It is worth running: a 30 second local session produced 26 inputs, 20 of which survived -merge=1 as coverage-increasing. The corpus is nowhere near saturated, and the defects this container has had -- #63, #65 through #70, #74 -- were exception safety and aliasing bugs of exactly the kind this reaches. The job seeds from data/fuzz/api but writes new inputs to a scratch directory, since libFuzzer writes to the first corpus directory it is given, so a run can never modify what is committed. Afterwards it does the same -merge=1 that scripts/fuzz_merge.sh does locally, so the uploaded artifact is the handful of inputs that add coverage rather than everything the fuzzer happened to keep. A crash fails the job and uploads the reproducer. Committing what it finds stays a human decision, documented in CONTRIBUTING.md along with the local workflow: a job that pushes to the repository needs write access it has no other reason to have, and the corpus is small on purpose. Also drops '-isystem /usr/lib64/clang/14.0.0/include/' from the fuzz arguments. That is a Fedora path for a clang that is eight major versions old, it exists on no CI runner, and it was only harmless because clang ignores an include directory that is not there. See #86. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4c735aa commit e0538c9

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

.github/workflows/fuzz.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Fuzz
2+
3+
# The corpus in data/fuzz/api is replayed by the normal test suite on every run. That is a
4+
# regression guard and by construction can only find what has already been found. Nothing was
5+
# doing the finding: fuzz_api is built by the sanitizers job and then never run as a fuzzer.
6+
#
7+
# It is worth running. A 30 second local session turned up 20 new coverage-increasing inputs,
8+
# so the committed corpus is nowhere near saturated.
9+
on:
10+
schedule:
11+
- cron: '41 2 * * *'
12+
workflow_dispatch:
13+
inputs:
14+
seconds:
15+
description: how long to fuzz for
16+
default: '600'
17+
18+
permissions:
19+
contents: read
20+
21+
env:
22+
MESON_PACKAGE_CACHE: subprojects/packagecache
23+
24+
jobs:
25+
fuzz:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/setup-python@v5
30+
with:
31+
python-version: '3.x'
32+
cache: pip
33+
cache-dependency-path: .github/workflows/requirements.txt
34+
- uses: actions/cache@v4
35+
with:
36+
path: ${{ env.MESON_PACKAGE_CACHE }}
37+
key: wraps-${{ hashFiles('subprojects/*.wrap') }}
38+
- run: pip install -r .github/workflows/requirements.txt
39+
40+
# fuzz_api only exists under clang, see the guard in test/meson.build.
41+
- run: meson setup builddir --force-fallback-for=fmt
42+
env:
43+
CXX: clang++
44+
- run: ninja -C builddir test/fuzz_api
45+
46+
# New inputs land in the scratch directory because libFuzzer writes to the first corpus
47+
# directory it is given; data/fuzz/api is only ever read here. Committing what this finds
48+
# stays a human decision, see CONTRIBUTING.md.
49+
- name: Fuzz
50+
run: |
51+
mkdir -p crashes corpus-scratch
52+
./builddir/test/fuzz_api \
53+
-max_total_time=${{ inputs.seconds || 600 }} \
54+
-print_final_stats=1 \
55+
-artifact_prefix=crashes/ \
56+
corpus-scratch data/fuzz/api
57+
58+
# A crash is the whole point of running this, and the reproducer is the valuable part, so
59+
# it has to survive the job. Given the bugs this container has had -- #63, #65 through #70,
60+
# #74, all exception safety and aliasing -- a find here is a real bug, not noise.
61+
- name: Upload the crash
62+
if: failure()
63+
uses: actions/upload-artifact@v4
64+
with:
65+
name: fuzz-crash
66+
path: crashes/
67+
68+
# -merge=1 keeps only inputs that add coverage, which is exactly what scripts/fuzz_merge.sh
69+
# does locally. Without it the artifact would be everything the fuzzer happened to keep
70+
# rather than the handful worth committing, and the corpus would grow without bound.
71+
- name: Minimize what was found
72+
run: |
73+
cp -r data/fuzz/api corpus-merged
74+
./builddir/test/fuzz_api -merge=1 corpus-merged corpus-scratch
75+
mkdir -p corpus-new
76+
for f in corpus-merged/*; do
77+
[ -e "data/fuzz/api/$(basename "$f")" ] || cp "$f" corpus-new/
78+
done
79+
echo "$(ls corpus-new | wc -l) new coverage-increasing inputs" >> "$GITHUB_STEP_SUMMARY"
80+
81+
- name: Upload the new corpus entries
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: corpus-new
85+
path: corpus-new/
86+
if-no-files-found: ignore

CONTRIBUTING.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ You want to contribute? Awesome!
77
3. Format the code with `clang-format`
88
4. create a PR
99

10+
## Fuzzing
11+
12+
`data/fuzz/api` is a minimized corpus that the normal test suite replays on every run. That
13+
guards against regressions but never finds anything new, so a nightly job does the finding. It
14+
can also be started by hand from the Actions tab, with a duration.
15+
16+
To fuzz locally, from a clang build directory:
17+
18+
```sh
19+
CXX=clang++ meson setup builddir
20+
cd builddir
21+
../scripts/fuzz_run.sh api # accumulates into CORPUS_BIG/
22+
../scripts/fuzz_merge.sh api # folds back only what adds coverage
23+
```
24+
25+
**New corpus entries are committed by hand, never by CI.** The nightly job uploads what it finds
26+
as a `corpus-new` artifact and stops there, because a job that pushes to the repository needs
27+
write access it has no other reason to have. Download the artifact, drop the files into
28+
`data/fuzz/api`, check the suite still passes, and commit them.
29+
30+
A crash fails the job and uploads the reproducer as a `fuzz-crash` artifact. Reproduce it with
31+
`./builddir/test/fuzz_api <file>`. Treat it as a real bug: most of the defects this container
32+
has had were exception safety and aliasing problems found this way rather than reported.
33+
1034
## Developer Certificate of Origin (DCO)
1135

1236
All contributions (including pull requests) must agree to the [Developer Certificate of Origin (DCO) version 1.1](https://developercertificate.org). This is a developer's certification that he or she has the right to submit the patch for inclusion into the project.

test/meson.build

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ if compiler.get_id() == 'clang'
127127
'-fsanitize-undefined-trap-on-error',
128128
'-fsanitize=undefined,address,fuzzer',
129129
'-g',
130-
'-isystem', '/usr/lib64/clang/14.0.0/include/',
131130
]
132131
fuzz_link_args = ['-fsanitize=undefined,address,fuzzer']
133132
fuzz_sources = [

0 commit comments

Comments
 (0)