1818 - ' tests/contract_property_tests.rs'
1919 - ' .github/workflows/fuzzing.yml'
2020 - ' Cargo.toml'
21+ # Nightly, so the AddressSanitizer smoke stage still runs regularly without
22+ # sitting on the critical path of every pull request.
23+ schedule :
24+ - cron : ' 0 3 * * *'
25+
2126 # Allow manual dispatch with configurable fuzz duration.
2227 workflow_dispatch :
2328 inputs :
@@ -110,28 +115,33 @@ jobs:
110115 run : cargo build --locked
111116
112117 # ── 3. Short fuzz runs (sanity / smoke) ─────────────────────────────────────
118+ #
119+ # Deliberately a single job rather than a matrix. This stage previously fanned
120+ # out to one job per target, and each job independently rebuilt the entire
121+ # dependency tree under AddressSanitizer before it could fuzz anything. Those
122+ # jobs were skipped for the whole life of this workflow (they `needs:
123+ # fuzz-build`, which was failing), so the cost was never observed; the first
124+ # run that reached them had all 13 killed with SIGTERM after 16-23 minutes,
125+ # still compiling, without a single target ever being fuzzed.
126+ #
127+ # `cargo fuzz build` compiles every target in one pass, so the sanitizer build
128+ # is paid once and each target then runs from the built binary.
113129 fuzz-smoke :
114- name : Fuzz Smoke Run (${{ matrix.target }})
130+ name : Fuzz Smoke Run
115131 runs-on : ubuntu-latest
116132 needs : fuzz-build
117- strategy :
118- fail-fast : false
119- matrix :
120- target :
121- - fuzz_validate_public_key
122- - fuzz_validate_secret_key
123- - fuzz_validate_contract_id
124- - fuzz_validate_wallet_name
125- - fuzz_validate_amount
126- - fuzz_passphrase_strength
127- - fuzz_wasm_hash
128- - fuzz_encrypted_bundle_parse
129- - fuzz_template_operations
130- # Contract fuzzing harnesses
131- - fuzz_wasm_validation
132- - fuzz_contract_invocation
133- - fuzz_contract_spec_parse
134- - fuzz_test_generator
133+ # Not on pull requests. Instrumenting this dependency tree with
134+ # AddressSanitizer and then compiling `starforge` -- one very large crate --
135+ # exceeds the memory of a standard 16 GB runner, and the job is killed
136+ # part-way through the build with no diagnostic in the log. Capping build
137+ # parallelism only moved the failure from the leaf crates to `starforge`
138+ # itself, and disk was never the constraint (85 GB free).
139+ #
140+ # `Build Fuzz Harnesses` still compiles every harness on each PR, so a
141+ # harness that stops building is caught there. What moves off the PR path is
142+ # only the act of running them, which now happens nightly and on demand.
143+ if : github.event_name != 'pull_request'
144+ timeout-minutes : 90
135145 steps :
136146 - uses : actions/checkout@v4
137147
@@ -155,23 +165,59 @@ jobs:
155165 ~/.cargo/registry
156166 ~/.cargo/git
157167 fuzz/target
158- key : ${{ runner.os }}-fuzz-smoke-${{ matrix.target }}-${{ hashFiles('** /Cargo.lock') }}
168+ key : ${{ runner.os }}-fuzz-smoke-${{ hashFiles('fuzz /Cargo.lock') }}
159169 restore-keys : |
160- ${{ runner.os }}-fuzz-smoke-${{ matrix.target }}-
170+ ${{ runner.os }}-fuzz-smoke-
171+
172+ - name : Build all fuzz targets
173+ env :
174+ # Fewer parallel rustc processes, each holding less peak memory.
175+ CARGO_BUILD_JOBS : ' 2'
176+ run : cargo fuzz build --fuzz-dir fuzz
161177
162- - name : Run fuzz target (${{ matrix.target }})
178+ - name : Run each fuzz target
163179 run : |
164180 DURATION=${{ github.event.inputs.fuzz_duration || '30' }}
165- cargo fuzz run ${{ matrix.target }} \
166- --fuzz-dir fuzz \
167- -- -max_total_time=${DURATION} -max_len=4096
181+ TARGETS="
182+ fuzz_validate_public_key
183+ fuzz_validate_secret_key
184+ fuzz_validate_contract_id
185+ fuzz_validate_wallet_name
186+ fuzz_validate_amount
187+ fuzz_passphrase_strength
188+ fuzz_wasm_hash
189+ fuzz_encrypted_bundle_parse
190+ fuzz_template_operations
191+ fuzz_wasm_validation
192+ fuzz_contract_invocation
193+ fuzz_contract_spec_parse
194+ fuzz_test_generator
195+ "
196+ failed=""
197+ for target in $TARGETS; do
198+ echo "::group::$target"
199+ # The build above is already done, so this only fuzzes.
200+ if cargo fuzz run "$target" --fuzz-dir fuzz \
201+ -- -max_total_time="$DURATION" -max_len=4096; then
202+ echo " PASS $target"
203+ else
204+ echo " FAIL $target"
205+ failed="$failed $target"
206+ fi
207+ echo "::endgroup::"
208+ done
209+ if [ -n "$failed" ]; then
210+ echo "Fuzz targets reported findings:$failed"
211+ exit 1
212+ fi
213+ echo "All fuzz targets completed without findings."
168214
169215 - name : Upload corpus artifacts on failure
170216 if : failure()
171217 uses : actions/upload-artifact@v4
172218 with :
173- name : fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
174- path : fuzz/corpus/${{ matrix.target }}/
219+ name : fuzz-corpus-${{ github.run_id }}
220+ path : fuzz/corpus/
175221
176222 # ── 4. Coverage reporting ─────────────────────────────────────────────────────
177223 coverage :
0 commit comments