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
name: fuzz
# The explorer half of the fuzzing setup. The gate half --
# tests/fuzz_decoders.rs -- runs in the `test` job of ci.yml on every
# pull request, because it is deterministic and finishes in five
# seconds. This one is neither, and that is why it is here instead.
#
# A fuzzer's result depends on how long it ran. Putting it in the pull
# request gate would mean a fresh finding fails whichever unrelated
# change happened to be open, and a green result would only ever mean
# "nothing found in the seconds we could spare".
#
# So it runs nightly and on demand, and when it finds something the
# reproducer is uploaded. That reproducer belongs in fuzz/corpus/, where
# the gate replays it on every pull request from then on.
#
# ext4 is mounted from disks and images this reader did not write -- a
# USB stick, a VM image, a card out of a device -- and every structure
# on it is read from an offset the structure before it supplied, which
# is exactly the threat model a fuzzer is for.
on:
workflow_dispatch:
inputs:
seconds:
description: "Seconds per target"
required: false
default: "120"
schedule:
- cron: "0 3 * * *"
# Declared, so the job does not inherit whatever the repository's default
# token grants: tests/scripts/test-write-jobs-pinned.sh treats a workflow
# with no `permissions` as one that may write.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CHORE_VERSION: "0.11.0"
jobs:
fuzz:
name: fuzz the superblock, inode, directory and journal decoders
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
# The siblings come through `chore`, at the refs pinned in
# chores.yml, the same way ci.yml gets them -- rather than a
# second place that says which version of rust-fs-core this
# repository builds against.
- name: Install chore
run: scripts/ci-install-chore.sh
- name: chore siblings
run: chore siblings
# Nightly, not the pin in rust-toolchain.toml. cargo-fuzz needs
# `-Z sanitizer`, which stable does not have. The explicit
# `+nightly` in scripts/fuzz-all.sh overrides the pin for these
# invocations and nothing else.
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
# Building cargo-fuzz takes several minutes and changes about as
# often as the compiler does, so it is cached on its own version
# rather than on this repository's lockfile.
- name: Cache cargo-fuzz
id: cache-cargo-fuzz
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-fuzz
key: cargo-fuzz-${{ runner.os }}-${{ runner.arch }}-v1
- name: Install cargo-fuzz
if: steps.cache-cargo-fuzz.outputs.cache-hit != 'true'
run: cargo +nightly install cargo-fuzz --locked
- name: Show what the corpus holds
run: |
find fuzz/corpus -type f | sort
echo "total: $(find fuzz/corpus -type f | wc -l) seeds"
# The input arrives through env:, never pasted into the script: an
# expression in `run:` is substituted before the shell parses it,
# so `1; curl ... | sh` would be commands. scripts/fuzz-all.sh then
# refuses anything but a whole number of seconds that fits the
# job's timeout, before anything is fuzzed.
- name: Fuzz
env:
SECONDS_PER_TARGET: ${{ inputs.seconds || '120' }}
run: ./scripts/fuzz-all.sh "$SECONDS_PER_TARGET"
# WITHOUT THIS THE FINDING IS LOST. A crash reproducer lives in
# fuzz/artifacts/<target>/ on the runner, and the runner is thrown
# away. The log would say a target crashed and the bytes that
# caused it would be gone, leaving the next person to re-derive an
# input the fuzzer had already found. `cancelled()` too: a run
# stopped at its timeout still holds every crash the earlier
# targets found.
- name: Keep the reproducers
if: failure() || cancelled()
uses: actions/upload-artifact@v4
with:
name: fuzz-artifacts
path: fuzz/artifacts/
if-no-files-found: ignore
retention-days: 90