diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..abd7309 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,57 @@ +name: Fuzz + +permissions: + contents: read + +on: + push: + branches: + - master + pull_request: + schedule: + # Daily at 03:00 UTC + - cron: '0 3 * * *' + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + CI: 1 + CARGO_FUZZ_VERSION: 0.13.1 + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + fuzz: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - patch_from_str + - patch_from_bytes + - patch_set_gitdiff + - patch_set_unidiff + - patch_set_gitdiff_bytes + - patch_set_unidiff_bytes + steps: + - uses: actions/checkout@v6 + + - run: rustup toolchain install nightly + - run: rustup default nightly + + - uses: dtolnay/install@cargo-fuzz + + - run: cargo fuzz build ${{ matrix.target }} + - name: Run fuzzer + run: cargo fuzz run ${{ matrix.target }} -- -max_total_time=$TIME + env: + TIME: ${{ github.event_name == 'schedule' && '600' || '30' }} + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: fuzz-artifacts-${{ matrix.target }}-${{ github.sha }} + path: fuzz/artifacts/ diff --git a/Cargo.toml b/Cargo.toml index e0d116c..89eaab2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["diff", "patch", "merge"] categories = ["text-processing"] rust-version = "1.85.0" edition = "2024" +exclude = ["/fuzz/"] [package.metadata.docs.rs] # To build locally: diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..c5e3aba --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,135 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "diffy" +version = "0.4.2" +dependencies = [ + "hashbrown", + "zlib-rs", +] + +[[package]] +name = "diffy-fuzz" +version = "0.0.0" +dependencies = [ + "diffy", + "libfuzzer-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..7d16ca6 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "diffy-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.diffy] +path = ".." +features = ["binary"] + +[[bin]] +name = "patch_from_str" +path = "fuzz_targets/patch_from_str.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "patch_from_bytes" +path = "fuzz_targets/patch_from_bytes.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "patch_set_gitdiff" +path = "fuzz_targets/patch_set_gitdiff.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "patch_set_unidiff" +path = "fuzz_targets/patch_set_unidiff.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "patch_set_gitdiff_bytes" +path = "fuzz_targets/patch_set_gitdiff_bytes.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "patch_set_unidiff_bytes" +path = "fuzz_targets/patch_set_unidiff_bytes.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..8fb6eeb --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,49 @@ +# Fuzzing + +Uses [cargo-fuzz] with libFuzzer. + +[cargo-fuzz]: https://github.com/rust-fuzz/cargo-fuzz + +## Setup + +```bash +cargo +nightly install cargo-fuzz +``` + +## Run + +```bash +# List targets +cargo +nightly fuzz list + +# Run specific target (indefinitely) +cargo +nightly fuzz run patch_from_str + +# Run with time limit (seconds) +cargo +nightly fuzz run patch_from_str -- -max_total_time=60 + +# Run all targets (quick smoke test) +for t in $(cargo +nightly fuzz list); do + cargo +nightly fuzz run $t -- -max_total_time=10 +done +``` + +## Targets + +| Target | Tests | +|---------------------------|-----------------------------------------| +| `patch_from_str` | `Patch::from_str()` | +| `patch_from_bytes` | `Patch::from_bytes()` | +| `patch_set_gitdiff` | `PatchSet::parse(..., gitdiff())` | +| `patch_set_unidiff` | `PatchSet::parse(..., unidiff())` | +| `patch_set_gitdiff_bytes` | `PatchSet::parse_bytes(..., gitdiff())` | +| `patch_set_unidiff_bytes` | `PatchSet::parse_bytes(..., unidiff())` | + +## Crashes + +Crash inputs are saved to `fuzz/artifacts//`. +To reproduce: + +```bash +cargo +nightly fuzz run fuzz/artifacts//crash- +``` diff --git a/fuzz/fuzz_targets/patch_from_bytes.rs b/fuzz/fuzz_targets/patch_from_bytes.rs new file mode 100644 index 0000000..da529f4 --- /dev/null +++ b/fuzz/fuzz_targets/patch_from_bytes.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Should never panic - only return Ok or Err + let _ = diffy::Patch::from_bytes(data); +}); diff --git a/fuzz/fuzz_targets/patch_from_str.rs b/fuzz/fuzz_targets/patch_from_str.rs new file mode 100644 index 0000000..3ff199a --- /dev/null +++ b/fuzz/fuzz_targets/patch_from_str.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &str| { + // Should never panic - only return Ok or Err + let _ = diffy::Patch::from_str(data); +}); diff --git a/fuzz/fuzz_targets/patch_set_gitdiff.rs b/fuzz/fuzz_targets/patch_set_gitdiff.rs new file mode 100644 index 0000000..af70abe --- /dev/null +++ b/fuzz/fuzz_targets/patch_set_gitdiff.rs @@ -0,0 +1,11 @@ +#![no_main] + +use diffy::patch_set::{ParseOptions, PatchSet}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &str| { + for result in PatchSet::parse(data, ParseOptions::gitdiff()) { + // Consume every item to avoid short-circuiting on first `Err`. + let _ = result; + } +}); diff --git a/fuzz/fuzz_targets/patch_set_gitdiff_bytes.rs b/fuzz/fuzz_targets/patch_set_gitdiff_bytes.rs new file mode 100644 index 0000000..67ead43 --- /dev/null +++ b/fuzz/fuzz_targets/patch_set_gitdiff_bytes.rs @@ -0,0 +1,11 @@ +#![no_main] + +use diffy::patch_set::{ParseOptions, PatchSet}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Consume every item to avoid short-circuiting on first `Err`. + for result in PatchSet::parse_bytes(data, ParseOptions::gitdiff()) { + let _ = result; + } +}); diff --git a/fuzz/fuzz_targets/patch_set_unidiff.rs b/fuzz/fuzz_targets/patch_set_unidiff.rs new file mode 100644 index 0000000..1e1671c --- /dev/null +++ b/fuzz/fuzz_targets/patch_set_unidiff.rs @@ -0,0 +1,11 @@ +#![no_main] + +use diffy::patch_set::{ParseOptions, PatchSet}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &str| { + for result in PatchSet::parse(data, ParseOptions::unidiff()) { + // Consume every item to avoid short-circuiting on first `Err`. + let _ = result; + } +}); diff --git a/fuzz/fuzz_targets/patch_set_unidiff_bytes.rs b/fuzz/fuzz_targets/patch_set_unidiff_bytes.rs new file mode 100644 index 0000000..81083a4 --- /dev/null +++ b/fuzz/fuzz_targets/patch_set_unidiff_bytes.rs @@ -0,0 +1,11 @@ +#![no_main] + +use diffy::patch_set::{ParseOptions, PatchSet}; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Consume every item to avoid short-circuiting on first `Err`. + for result in PatchSet::parse_bytes(data, ParseOptions::unidiff()) { + let _ = result; + } +});