From 217a700bfa47b7b6e963fceb51a156b8c525880e Mon Sep 17 00:00:00 2001 From: Alex Gorevski Date: Tue, 17 Feb 2026 12:12:08 -0800 Subject: [PATCH] ci: add fuzz testing workflow and harnesses (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Security-critical parsing surfaces (config loading, tool parameter deserialization) have no fuzz testing coverage. Malformed inputs to these surfaces could cause panics, memory issues, or unexpected behavior in production. Solution: Add a weekly cargo-fuzz CI workflow with two initial harnesses: - fuzz_config_parse: fuzzes TOML config deserialization - fuzz_tool_params: fuzzes JSON tool parameter parsing The workflow runs each target for 300 seconds (configurable via workflow_dispatch input), uses nightly Rust toolchain (required by libfuzzer), and uploads crash artifacts for triage with 30-day retention. Step summaries report pass/fail status per target. Files added: - .github/workflows/fuzz.yml (scheduled + manual dispatch) - fuzz/Cargo.toml (fuzz crate manifest) - fuzz/fuzz_targets/fuzz_config_parse.rs - fuzz/fuzz_targets/fuzz_tool_params.rs Testing: Validated YAML syntax and Cargo.toml structure. Fuzz harnesses use standard libfuzzer-sys patterns. Actual fuzzing will execute on first scheduled or manual CI run. Ref: zeroclaw-labs/zeroclaw#618 (item 4 — Fuzz Testing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/fuzz.yml | 72 ++++++++++++++++++++++++++ fuzz/Cargo.toml | 26 ++++++++++ fuzz/fuzz_targets/fuzz_config_parse.rs | 9 ++++ fuzz/fuzz_targets/fuzz_tool_params.rs | 9 ++++ 4 files changed, 116 insertions(+) create mode 100644 .github/workflows/fuzz.yml create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/fuzz_config_parse.rs create mode 100644 fuzz/fuzz_targets/fuzz_tool_params.rs diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..a8a19b0 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,72 @@ +name: Fuzz Testing + +on: + schedule: + - cron: "0 2 * * 0" # Weekly Sunday 2am UTC + workflow_dispatch: + inputs: + fuzz_seconds: + description: "Seconds to run each fuzz target" + required: false + default: "300" + +concurrency: + group: fuzz-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + issues: write + +env: + CARGO_TERM_COLOR: always + +jobs: + fuzz: + name: Fuzz (${{ matrix.target }}) + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + target: + - fuzz_config_parse + - fuzz_tool_params + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: nightly + components: llvm-tools-preview + + - name: Install cargo-fuzz + run: cargo install cargo-fuzz --locked + + - name: Run fuzz target + run: | + SECONDS="${{ github.event.inputs.fuzz_seconds || '300' }}" + echo "Fuzzing ${{ matrix.target }} for ${SECONDS}s" + cargo +nightly fuzz run ${{ matrix.target }} -- \ + -max_total_time="${SECONDS}" \ + -max_len=4096 + continue-on-error: true + id: fuzz + + - name: Upload crash artifacts + if: failure() || steps.fuzz.outcome == 'failure' + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: fuzz-crashes-${{ matrix.target }} + path: fuzz/artifacts/${{ matrix.target }}/ + retention-days: 30 + if-no-files-found: ignore + + - name: Report fuzz results + run: | + echo "### Fuzz: ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY" + if [ "${{ steps.fuzz.outcome }}" = "failure" ]; then + echo "- :x: Crashes found — see artifacts" >> "$GITHUB_STEP_SUMMARY" + else + echo "- :white_check_mark: No crashes found" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..b9d2bbe --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "zeroclaw-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.zeroclaw] +path = ".." + +[[bin]] +name = "fuzz_config_parse" +path = "fuzz_targets/fuzz_config_parse.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_tool_params" +path = "fuzz_targets/fuzz_tool_params.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_config_parse.rs b/fuzz/fuzz_targets/fuzz_config_parse.rs new file mode 100644 index 0000000..6a94fa2 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_config_parse.rs @@ -0,0 +1,9 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Fuzz TOML config parsing — silently discard invalid input + let _ = toml::from_str::(s); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_tool_params.rs b/fuzz/fuzz_targets/fuzz_tool_params.rs new file mode 100644 index 0000000..93c4cf3 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_tool_params.rs @@ -0,0 +1,9 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + // Fuzz JSON tool parameter parsing — silently discard invalid input + let _ = serde_json::from_str::(s); + } +});