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); + } +});