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
208
209
210
211
212
213
214
215
216
name: Fuzzing
on:
schedule:
# Run daily at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
pull_request:
paths:
- 'src/**'
- 'fuzz/**'
- '.github/workflows/fuzz.yml'
env:
CARGO_TERM_COLOR: always
jobs:
fuzz:
name: Fuzz Testing
runs-on: ubuntu-latest
strategy:
matrix:
target:
- protocol_parsing
- jsonrpc_handling
- transport_layer
- auth_flows
# Phase 125-05: SEP-2640 skill entry synthesis. Registering a target
# in fuzz/Cargo.toml does NOT enrol it here — this matrix is an
# explicit list, and several registered targets are deliberately
# absent from it. The CLAUDE.md ALWAYS/FUZZ requirement's substance is
# RECURRING execution against hostile input; a registered-but-unrun
# target satisfies only its letter. Arbitrary author-supplied bytes
# reaching serde_yaml is exactly what a scheduled campaign is for.
- fuzz_skill_entry
# Phase 126-03: the workflow-to-skill projection. Same reasoning as
# the row above, one boundary further in — `as_skill()` is public and
# INFALLIBLE, so author prose reaching its slug truncation and its
# YAML frontmatter encoder has a panic as its only failure mode, and
# those bytes are published as a sha256 pin (D-14).
- fuzz_workflow_projection
# Phase 128-10 (SC-7 / T-128-47): the `tools/call` INPUT-schema
# enforcement path and its value-free refusal renderer. Same reasoning
# as the two rows above — a registered-but-unrun target satisfies only
# the letter of the CLAUDE.md ALWAYS/FUZZ requirement — with one
# addition specific to this pair. These two are ALSO run by
# `make test-fuzz-strict`, which `make quality-gate` chains and which
# the `quality-gate` CI job provisions nightly + cargo-fuzz for. That
# is the PR gate; this matrix is the recurring DEEP campaign
# (`-max_total_time=300` versus the gate's short bound), and the two
# are complementary rather than redundant.
#
# MEASURED 2026-09-27: the org ruleset "Green Main — unified gate
# enforcement" requires exactly ONE status context, `gate`. This
# workflow is NOT a required check, so enrolling here alone would give
# recurring execution and no merge gate — which is why the leg is
# provisioned in `ci.yml`'s quality-gate job as well. Do not read this
# matrix row as the gate.
- fuzz_input_schema_enforcement
# Phase 128-10 (T-128-48): the LOOK at RESEARCH assumption A2 — a
# config-supplied `pattern` as a ReDoS vector. The daily schedule is
# most of this target's value: A2 was not falsified at n <= 28, and a
# recurring campaign is how a residual that needs more budget than a
# PR gate can spend would surface. A timeout here is a finding to FILE,
# not a licence to hand-roll a regex guard — see the target's header.
- fuzz_placeholder_pattern_redos
steps:
- uses: actions/checkout@v7
- name: Remove rust-toolchain.toml to allow nightly
run: rm -f rust-toolchain.toml
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: llvm-tools-preview
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Cache fuzz corpus
uses: actions/cache@v6
with:
path: fuzz/corpus
key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}
restore-keys: |
fuzz-corpus-${{ matrix.target }}-
- name: Run fuzzing (${{ matrix.target }})
run: |
cargo fuzz run ${{ matrix.target }} -- \
-max_total_time=300 \
-print_final_stats=1 \
-detect_leaks=0
# libFuzzer caps active fuzzing at 300s. The step budget covers
# corpus flush + actions/cache save + sanitizer shutdown tail —
# which can spike when a target's input space expands (e.g.
# adding a `HashMap<String, Value>` field) and a single run adds
# thousands of new corpus entries. 15 min gives that tail room
# to land without losing CI signal.
timeout-minutes: 15
- name: Minimize corpus
if: github.event_name == 'schedule'
run: cargo fuzz cmin ${{ matrix.target }}
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: fuzz-crashes-${{ matrix.target }}
path: fuzz/artifacts/${{ matrix.target }}/
- name: Upload corpus
if: github.event_name == 'schedule'
uses: actions/upload-artifact@v7
with:
name: fuzz-corpus-${{ matrix.target }}
path: fuzz/corpus/${{ matrix.target }}/
fuzz-coverage:
name: Fuzzing Coverage
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v7
- name: Remove rust-toolchain.toml to allow nightly
run: rm -f rust-toolchain.toml
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: llvm-tools-preview
- name: Install tools
run: |
cargo install cargo-fuzz
cargo install rustfilt
- name: Cache fuzz corpus
uses: actions/cache@v6
with:
path: fuzz/corpus
key: fuzz-corpus-all-${{ github.sha }}
restore-keys: |
fuzz-corpus-all-
fuzz-corpus-
- name: Generate minimal corpus if needed
run: |
for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
# Create corpus directory if it doesn't exist
mkdir -p fuzz/corpus/$target
# If corpus is empty, run fuzzer briefly to generate some inputs
if [ -z "$(ls -A fuzz/corpus/$target 2>/dev/null)" ]; then
echo "No corpus found for $target, generating minimal corpus..."
cargo fuzz run $target -- \
-max_total_time=10 \
-print_final_stats=1 \
-detect_leaks=0 || true
fi
done
- name: Generate coverage
run: |
for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
cargo fuzz coverage $target
done
- name: Upload coverage reports
uses: actions/upload-artifact@v7
with:
name: fuzz-coverage
path: fuzz/coverage/
fuzz-24h:
name: 24-Hour Fuzzing
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v7
- name: Remove rust-toolchain.toml to allow nightly
run: rm -f rust-toolchain.toml
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- name: Install cargo-fuzz
run: cargo install cargo-fuzz
- name: Run 24-hour fuzzing
run: |
# Run each target for 6 hours (total 24 hours)
for target in protocol_parsing jsonrpc_handling transport_layer auth_flows; do
echo "Starting 6-hour fuzz for $target"
cargo fuzz run $target -- \
-max_total_time=21600 \
-print_final_stats=1 \
-detect_leaks=0 || true
done
timeout-minutes: 1440 # 24 hours
- name: Upload results
uses: actions/upload-artifact@v7
with:
name: fuzz-24h-results
path: |
fuzz/corpus/
fuzz/artifacts/
fuzz/*.log