├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── arbitrary │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── asan │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs └── url │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── src │ └── main.rs │ └── url.dict ├── src ├── bin │ └── cargo-ziggy │ │ ├── add_seeds.rs │ │ ├── build.rs │ │ ├── coverage.rs │ │ ├── fuzz.rs │ │ ├── main.rs │ │ ├── minimize.rs │ │ ├── plot.rs │ │ ├── run.rs │ │ └── triage.rs └── lib.rs └── tests ├── arbitrary_fuzz.rs ├── asan_fuzz.rs └── url_fuzz.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Cargo sort 16 | run: cargo install cargo-sort && cargo sort --check 17 | - name: Format 18 | run: cargo fmt --check 19 | - name: Clippy 20 | run: cargo clippy --workspace --all-targets -- -D warnings 21 | build_and_test: 22 | name: Rust project - latest 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | toolchain: 27 | - stable 28 | - beta 29 | - nightly 30 | steps: 31 | - uses: actions/checkout@v4 32 | - run: sudo apt-get update 33 | - run: sudo apt-get install binutils-dev libunwind-dev gnuplot 34 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 35 | - run: rustup component add rust-src 36 | - run: cargo install cargo-afl honggfuzz grcov 37 | - run: cargo install --path . --verbose 38 | - run: cargo test --verbose -- --show-output 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | core -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 1.3.2 - 4 | 5 | - add -m memory limit for AFL++ fuzz option 6 | - moved previous -m minimize option to -M 7 | - Update `afl.rs` to `0.15.16` 8 | 9 | ## 1.3.1 - 2025-02-26 10 | 11 | - AFL++ fuzz targets now can be used for replays by given input files as 12 | command line parameters 13 | 14 | ## 1.3.0 - 2024-11-05 15 | 16 | - Fix bugs (#102 and #103) 17 | - Add `--asan` fuzz option (#104) 18 | - Add `--coverage-worker` fuzz option (#96 and #105) 19 | - Improve message if AFL++ needs config (#106) 20 | - Remove dual minimization (#107) 21 | - Update `afl.rs` to `0.15.11` 22 | - Thank you @R9295 and @kevin-valerio for these changes! 23 | 24 | ## 1.2.1 - 2024-10-02 25 | 26 | - Add a "binary fuzzing mode" (#99) 27 | - Fix `RUSTFLAG` bug (#100) 28 | - Fix release mode bug (#101) 29 | - Update `afl.rs` to `0.15.10` 30 | - Thank you @R9295 and @Ollrogge for these changes! 31 | 32 | ## 1.2.0 - 2024-09-16 33 | 34 | - Add recursive option to `cargo ziggy run` (#93) 35 | - Add support for different grcov coverage output types (#94) 36 | - Add support for building a target in `--release` mode (#95) 37 | - Fix CI (#98) 38 | - Thank you @R9295 and @kevin-valerio for these changes! 39 | 40 | ## 1.1.0 - 2024-05-22 41 | 42 | - Upgrade honggfuzz version 43 | 44 | ## 1.0.2 - 2024-05-17 45 | 46 | - Downgrade dependency that was [yanked](https://github.com/rust-lang/libc/issues/3608#issuecomment-2116310436) 47 | 48 | ## 1.0.1 - 2024-05-07 49 | 50 | - Only make AFL++ sync to shared corpus if Honggfuzz is also running 51 | - Remove serde_json and toml dependencies 52 | - Update dependencies 53 | 54 | ## 1.0.0 - 2024-02-05 55 | 56 | - Improve `-a` flag to let you pass multiple arguments to AFL++ 57 | - Add `-C` configuration flag 58 | - Improve url fuzzer to showcase different fuzzing metodologies 59 | 60 | ## 0.8.3 - 2024-01-18 61 | 62 | - Add new flag for passing arguments directly to AFL++ 63 | - Run the build code before launching minimization 64 | - Fix GitHub CI 65 | 66 | ## 0.8.2 - 2023-11-20 67 | 68 | - Add option to optionally keep coverage data 69 | - Tweak TUI and prevent flickering 70 | - Improve add-seeds command 71 | - Update some dependencies 72 | 73 | ## 0.8.1 - 2023-10-30 74 | 75 | - Add forking coverage, to speedup coverage generation while allowing crashing inputs in corpus 76 | 77 | ## 0.8.0 - 2023-10-16 78 | 79 | - New and improved terminal user interface 80 | 81 | ## 0.7.2 - 2023-10-12 82 | 83 | - Add `-z, --ziggy-output` flag and `ZIGGY_OUTPUT` environment variable to set ziggy's output directory 84 | - Fix a couple of misbehaviours when building the fuzzers 85 | - Improve populating of main corpus 86 | - Add CI and tests 87 | - Improve documentation 88 | 89 | ## 0.7.1 - 2023-10-05 90 | 91 | - Fix honggfuzz bug 92 | - Cleanup minimization logic 93 | - Check grcov is installed before running coverage 94 | - Fix CLI output glitches 95 | 96 | ## 0.7.0 - 2023-09-28 97 | 98 | - Revamp CLI output 99 | - Make honggfuzz learn from AFL++ seeds on the fly 100 | - Remove initial minimization by default 101 | - Fix bug with add-seeds secondary fuzzer name 102 | - Improve AFL++ flags for more fuzzing diversity 103 | - Add coverage feature to the harness 104 | - Add casr triage functionality 105 | 106 | ## 0.6.8 - 2023-09-11 107 | 108 | - Fix bug with add-seeds determinism 109 | - Fix temporary corpus bug 110 | 111 | ## 0.6.7 - 2023-08-31 112 | 113 | - Add new command - `cargo ziggy add-seeds` 114 | - Tweak AFL++ flags for better performance 115 | - Coverage now continues running after finding crash 116 | 117 | ## 0.6.6 - 2023-08-29 118 | 119 | - Add CLI pointer to second AFL++ fuzzer log 120 | - Update dependencies, including the new AFL++ crate 121 | 122 | ## 0.6.5 - 2023-08-24 123 | 124 | - Secondary AFL++ fuzzer log is now available 125 | - Bump AFL++ version 126 | - Better AFL++ envs, thanks again @vanhauser-thc 127 | 128 | ## 0.6.4 - 2023-08-14 129 | 130 | - Better AFL++ envs, thank you @vanhauser-thc! 131 | - Bump AFL++ version 132 | - Honggfuzz share of total CPUs is now reduced 133 | - Overall code cleanup 134 | 135 | ## 0.6.3 - 2023-06-20 136 | 137 | - Add flag to skip initial minimization 138 | 139 | ## 0.6.2 - 2023-06-20 140 | 141 | - Fix parallel minimization bug 142 | 143 | ## 0.6.1 - 2023-06-20 144 | 145 | - Add parallel jobs for minimization 146 | - Add minimization at the beginning of fuzzing 147 | - Fix crash discovery code 148 | 149 | ## 0.6.0 - 2023-06-07 150 | 151 | - Remove no_main (pr #29, issue #28) 152 | - Remove useless code 153 | 154 | ## 0.5.0 - 2023-06-07 155 | 156 | - Update dependencies 157 | - Fix coverage bug (see #27) 158 | - Add better error handling and logs 159 | - Split cargo-ziggy into different source files 160 | - Remove statsd use for afl++ 161 | - Simplify console output while fuzzing 162 | - Fix some long-standing fuzzer failure bugs 163 | 164 | ## 0.4.4 - 2023-04-25 165 | 166 | - Fix error handling bug 167 | 168 | ## 0.4.3 - 2023-04-24 169 | 170 | - Fix dependency bug 171 | 172 | ## 0.4.2 - 2023-04-24 173 | 174 | - Fix honggfuzz interface not showing up in logs 175 | - Fix some coverage generation difficulties (see #23) 176 | - More verbose error handling (thanks @brunoproduit!) 177 | - New default minimization timeout 178 | - `--no-honggfuzz` and `--no-afl` flags 179 | - Remove unused `init` command 180 | - Fix inconsistent number of jobs (now `-j 4` will launch 4 threads, not 8) 181 | - Update dependencies 182 | 183 | ## 0.4.1 - 2023-03-07 184 | 185 | - Fix cargo ziggy run argument bug 186 | 187 | ## 0.4.0 - 2023-03-06 188 | 189 | - Remove libfuzzer and add a custom runner 190 | - Remove secondary afl logs 191 | - Remove need to use rust nightly 192 | 193 | ## 0.3.4 - 2023-02-08 194 | 195 | - Add -G and -g flags for max and min input sizes 196 | - Add deterministic fuzzing to some AFL++ instances 197 | - Update dependencies 198 | 199 | ## 0.3.3 - 2022-12-13 200 | 201 | - Only run statsd on the main instance 202 | - Fix small display bug 203 | 204 | ## 0.3.2 - 2022-12-01 205 | 206 | - Fix crash directory bug 207 | 208 | ## 0.3.1 - 2022-11-30 209 | 210 | - Fix CLI output bug 211 | 212 | ## 0.3.0 - 2022-11-29 213 | 214 | - Add support for #[cfg(fuzzing)] and #[cfg(not(fuzzing))] 215 | - Add warning for AFL++ kernel and CPU rules (#6) 216 | - Change input corpus argument in the run subcommand 217 | - Add source option for coverage generation (#8) 218 | - Add crash aggregation directory (#3) 219 | - Add variable to track if crashes were found (#10) 220 | - Fix behaviour when user stops fuzzing in the middle of minimization (#7) 221 | - Add `plot` subcommand using afl-plot (#5) 222 | - Add initial corpus directory argument for fuzzing (#9) 223 | 224 | ## 0.2.3 - 2022-10-24 225 | 226 | - Update dependencies (fixes yanked dependency issue) 227 | 228 | ## 0.2.2 - 2022-10-17 229 | 230 | - Move logs to a `logs` directory (#4) 231 | - Automatically select target if possible (#1) 232 | 233 | ## 0.2.1 - 2022-09-23 234 | 235 | - Add reset_lazy static option support for better AFL++ stability 236 | - Update dependencies 237 | 238 | ## 0.2.0 - 2022-09-15 239 | 240 | - Let fuzzers continue after crash is found 241 | - Add Arbitrary support 242 | - Create different output directories for different fuzzing targets 243 | - Improve TUI 244 | - Use clap's derive syntax for the CLI code 245 | - Various bug fixes and small improvements 246 | 247 | ## 0.1.9 - 2022-09-01 248 | 249 | - Remove useless llvm flag for honggfuzz 250 | - Add `--no-libfuzzer` flag to skip building/fuzzing with libfuzzer 251 | 252 | ## 0.1.8 - 2022-08-08 253 | 254 | - Reset most of AFL's stats after each minimization for better corpus management 255 | 256 | ## 0.1.7 - 2022-08-04 257 | 258 | - Fix corpus coverage bug 259 | 260 | ## 0.1.6 - 2022-08-04 261 | 262 | - Add basic code coverage report generation 263 | 264 | ## 0.1.5 - 2022-08-04 265 | 266 | - Fix timeout bug 267 | 268 | ## 0.1.4 - 2022-08-03 269 | 270 | - Fix AFL++ timeout bug 271 | 272 | ## 0.1.3 - 2022-08-02 273 | 274 | - Rename threads to jobs 275 | 276 | ## 0.1.2 - 2022-08-02 277 | 278 | - Fix features usability issue 279 | 280 | ## 0.1.1 - 2022-08-02 281 | 282 | - Introduce the first stable version of ziggy 283 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "afl" 7 | version = "0.15.16" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "374ce830a032af7091cb68081644c991adb0e7636f9528c503beaa2a06162602" 10 | dependencies = [ 11 | "home", 12 | "libc", 13 | "rustc_version", 14 | "xdg", 15 | ] 16 | 17 | [[package]] 18 | name = "anstream" 19 | version = "0.6.15" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 22 | dependencies = [ 23 | "anstyle", 24 | "anstyle-parse", 25 | "anstyle-query", 26 | "anstyle-wincon", 27 | "colorchoice", 28 | "is_terminal_polyfill", 29 | "utf8parse", 30 | ] 31 | 32 | [[package]] 33 | name = "anstyle" 34 | version = "1.0.8" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 37 | 38 | [[package]] 39 | name = "anstyle-parse" 40 | version = "0.2.5" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 43 | dependencies = [ 44 | "utf8parse", 45 | ] 46 | 47 | [[package]] 48 | name = "anstyle-query" 49 | version = "1.1.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 52 | dependencies = [ 53 | "windows-sys", 54 | ] 55 | 56 | [[package]] 57 | name = "anstyle-wincon" 58 | version = "3.0.4" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 61 | dependencies = [ 62 | "anstyle", 63 | "windows-sys", 64 | ] 65 | 66 | [[package]] 67 | name = "anyhow" 68 | version = "1.0.89" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" 71 | 72 | [[package]] 73 | name = "arbitrary" 74 | version = "1.4.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 77 | dependencies = [ 78 | "derive_arbitrary", 79 | ] 80 | 81 | [[package]] 82 | name = "arbitrary-fuzz" 83 | version = "0.1.0" 84 | dependencies = [ 85 | "arbitrary", 86 | "ziggy", 87 | ] 88 | 89 | [[package]] 90 | name = "asan-fuzz" 91 | version = "0.1.0" 92 | dependencies = [ 93 | "ziggy", 94 | ] 95 | 96 | [[package]] 97 | name = "byteorder" 98 | version = "1.5.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 101 | 102 | [[package]] 103 | name = "camino" 104 | version = "1.1.9" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 107 | dependencies = [ 108 | "serde", 109 | ] 110 | 111 | [[package]] 112 | name = "cargo-platform" 113 | version = "0.1.8" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" 116 | dependencies = [ 117 | "serde", 118 | ] 119 | 120 | [[package]] 121 | name = "cargo_metadata" 122 | version = "0.18.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" 125 | dependencies = [ 126 | "camino", 127 | "cargo-platform", 128 | "semver", 129 | "serde", 130 | "serde_json", 131 | "thiserror", 132 | ] 133 | 134 | [[package]] 135 | name = "cfg-if" 136 | version = "1.0.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 139 | 140 | [[package]] 141 | name = "clap" 142 | version = "4.5.20" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 145 | dependencies = [ 146 | "clap_builder", 147 | "clap_derive", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_builder" 152 | version = "4.5.20" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 155 | dependencies = [ 156 | "anstream", 157 | "anstyle", 158 | "clap_lex", 159 | "strsim", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_derive" 164 | version = "4.5.18" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 167 | dependencies = [ 168 | "heck", 169 | "proc-macro2", 170 | "quote", 171 | "syn", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_lex" 176 | version = "0.7.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 179 | 180 | [[package]] 181 | name = "colorchoice" 182 | version = "1.0.2" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 185 | 186 | [[package]] 187 | name = "console" 188 | version = "0.15.8" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 191 | dependencies = [ 192 | "encode_unicode", 193 | "lazy_static", 194 | "libc", 195 | "unicode-width", 196 | "windows-sys", 197 | ] 198 | 199 | [[package]] 200 | name = "derive_arbitrary" 201 | version = "1.4.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" 204 | dependencies = [ 205 | "proc-macro2", 206 | "quote", 207 | "syn", 208 | ] 209 | 210 | [[package]] 211 | name = "encode_unicode" 212 | version = "0.3.6" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 215 | 216 | [[package]] 217 | name = "fork" 218 | version = "0.1.23" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "60e74d3423998a57e9d906e49252fb79eb4a04d5cdfe188fb1b7ff9fc076a8ed" 221 | dependencies = [ 222 | "libc", 223 | ] 224 | 225 | [[package]] 226 | name = "form_urlencoded" 227 | version = "1.2.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 230 | dependencies = [ 231 | "percent-encoding", 232 | ] 233 | 234 | [[package]] 235 | name = "getrandom" 236 | version = "0.2.15" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 239 | dependencies = [ 240 | "cfg-if", 241 | "libc", 242 | "wasi", 243 | ] 244 | 245 | [[package]] 246 | name = "glob" 247 | version = "0.3.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 250 | 251 | [[package]] 252 | name = "heck" 253 | version = "0.5.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 256 | 257 | [[package]] 258 | name = "home" 259 | version = "0.5.9" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 262 | dependencies = [ 263 | "windows-sys", 264 | ] 265 | 266 | [[package]] 267 | name = "honggfuzz" 268 | version = "0.5.57" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "fc563d4f41b17364d5c48ded509f2bcf1c3f6ae9c7f203055b4a5c325072d57e" 271 | dependencies = [ 272 | "arbitrary", 273 | "lazy_static", 274 | "memmap2", 275 | "rustc_version", 276 | "semver", 277 | ] 278 | 279 | [[package]] 280 | name = "idna" 281 | version = "0.5.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 284 | dependencies = [ 285 | "unicode-bidi", 286 | "unicode-normalization", 287 | ] 288 | 289 | [[package]] 290 | name = "is_terminal_polyfill" 291 | version = "1.70.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 294 | 295 | [[package]] 296 | name = "itoa" 297 | version = "1.0.11" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 300 | 301 | [[package]] 302 | name = "lazy_static" 303 | version = "1.5.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 306 | 307 | [[package]] 308 | name = "libc" 309 | version = "0.2.160" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" 312 | 313 | [[package]] 314 | name = "memchr" 315 | version = "2.7.4" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 318 | 319 | [[package]] 320 | name = "memmap2" 321 | version = "0.9.5" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 324 | dependencies = [ 325 | "libc", 326 | ] 327 | 328 | [[package]] 329 | name = "percent-encoding" 330 | version = "2.3.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 333 | 334 | [[package]] 335 | name = "ppv-lite86" 336 | version = "0.2.20" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 339 | dependencies = [ 340 | "zerocopy", 341 | ] 342 | 343 | [[package]] 344 | name = "proc-macro2" 345 | version = "1.0.88" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" 348 | dependencies = [ 349 | "unicode-ident", 350 | ] 351 | 352 | [[package]] 353 | name = "quote" 354 | version = "1.0.37" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 357 | dependencies = [ 358 | "proc-macro2", 359 | ] 360 | 361 | [[package]] 362 | name = "rand" 363 | version = "0.8.5" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 366 | dependencies = [ 367 | "libc", 368 | "rand_chacha", 369 | "rand_core", 370 | ] 371 | 372 | [[package]] 373 | name = "rand_chacha" 374 | version = "0.3.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 377 | dependencies = [ 378 | "ppv-lite86", 379 | "rand_core", 380 | ] 381 | 382 | [[package]] 383 | name = "rand_core" 384 | version = "0.6.4" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 387 | dependencies = [ 388 | "getrandom", 389 | ] 390 | 391 | [[package]] 392 | name = "rustc_version" 393 | version = "0.4.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 396 | dependencies = [ 397 | "semver", 398 | ] 399 | 400 | [[package]] 401 | name = "ryu" 402 | version = "1.0.18" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 405 | 406 | [[package]] 407 | name = "semver" 408 | version = "1.0.23" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 411 | dependencies = [ 412 | "serde", 413 | ] 414 | 415 | [[package]] 416 | name = "serde" 417 | version = "1.0.210" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 420 | dependencies = [ 421 | "serde_derive", 422 | ] 423 | 424 | [[package]] 425 | name = "serde_derive" 426 | version = "1.0.210" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn", 433 | ] 434 | 435 | [[package]] 436 | name = "serde_json" 437 | version = "1.0.128" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 440 | dependencies = [ 441 | "itoa", 442 | "memchr", 443 | "ryu", 444 | "serde", 445 | ] 446 | 447 | [[package]] 448 | name = "strip-ansi-escapes" 449 | version = "0.2.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" 452 | dependencies = [ 453 | "vte", 454 | ] 455 | 456 | [[package]] 457 | name = "strsim" 458 | version = "0.11.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 461 | 462 | [[package]] 463 | name = "syn" 464 | version = "2.0.79" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" 467 | dependencies = [ 468 | "proc-macro2", 469 | "quote", 470 | "unicode-ident", 471 | ] 472 | 473 | [[package]] 474 | name = "thiserror" 475 | version = "1.0.64" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 478 | dependencies = [ 479 | "thiserror-impl", 480 | ] 481 | 482 | [[package]] 483 | name = "thiserror-impl" 484 | version = "1.0.64" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 487 | dependencies = [ 488 | "proc-macro2", 489 | "quote", 490 | "syn", 491 | ] 492 | 493 | [[package]] 494 | name = "time-humanize" 495 | version = "0.1.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "3e32d019b4f7c100bcd5494e40a27119d45b71fba2b07a4684153129279a4647" 498 | 499 | [[package]] 500 | name = "tinyvec" 501 | version = "1.8.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 504 | dependencies = [ 505 | "tinyvec_macros", 506 | ] 507 | 508 | [[package]] 509 | name = "tinyvec_macros" 510 | version = "0.1.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 513 | 514 | [[package]] 515 | name = "twox-hash" 516 | version = "2.0.1" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "a6db6856664807f43c17fbaf2718e2381ac1476a449aa104f5f64622defa1245" 519 | dependencies = [ 520 | "rand", 521 | ] 522 | 523 | [[package]] 524 | name = "unicode-bidi" 525 | version = "0.3.17" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 528 | 529 | [[package]] 530 | name = "unicode-ident" 531 | version = "1.0.13" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 534 | 535 | [[package]] 536 | name = "unicode-normalization" 537 | version = "0.1.24" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 540 | dependencies = [ 541 | "tinyvec", 542 | ] 543 | 544 | [[package]] 545 | name = "unicode-width" 546 | version = "0.1.14" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 549 | 550 | [[package]] 551 | name = "url" 552 | version = "2.5.2" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 555 | dependencies = [ 556 | "form_urlencoded", 557 | "idna", 558 | "percent-encoding", 559 | ] 560 | 561 | [[package]] 562 | name = "url-fuzz" 563 | version = "0.1.0" 564 | dependencies = [ 565 | "url", 566 | "ziggy", 567 | ] 568 | 569 | [[package]] 570 | name = "utf8parse" 571 | version = "0.2.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 574 | 575 | [[package]] 576 | name = "vte" 577 | version = "0.11.1" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" 580 | dependencies = [ 581 | "utf8parse", 582 | "vte_generate_state_changes", 583 | ] 584 | 585 | [[package]] 586 | name = "vte_generate_state_changes" 587 | version = "0.1.2" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" 590 | dependencies = [ 591 | "proc-macro2", 592 | "quote", 593 | ] 594 | 595 | [[package]] 596 | name = "wasi" 597 | version = "0.11.0+wasi-snapshot-preview1" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 600 | 601 | [[package]] 602 | name = "windows-sys" 603 | version = "0.52.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 606 | dependencies = [ 607 | "windows-targets", 608 | ] 609 | 610 | [[package]] 611 | name = "windows-targets" 612 | version = "0.52.6" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 615 | dependencies = [ 616 | "windows_aarch64_gnullvm", 617 | "windows_aarch64_msvc", 618 | "windows_i686_gnu", 619 | "windows_i686_gnullvm", 620 | "windows_i686_msvc", 621 | "windows_x86_64_gnu", 622 | "windows_x86_64_gnullvm", 623 | "windows_x86_64_msvc", 624 | ] 625 | 626 | [[package]] 627 | name = "windows_aarch64_gnullvm" 628 | version = "0.52.6" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 631 | 632 | [[package]] 633 | name = "windows_aarch64_msvc" 634 | version = "0.52.6" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 637 | 638 | [[package]] 639 | name = "windows_i686_gnu" 640 | version = "0.52.6" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 643 | 644 | [[package]] 645 | name = "windows_i686_gnullvm" 646 | version = "0.52.6" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 649 | 650 | [[package]] 651 | name = "windows_i686_msvc" 652 | version = "0.52.6" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 655 | 656 | [[package]] 657 | name = "windows_x86_64_gnu" 658 | version = "0.52.6" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 661 | 662 | [[package]] 663 | name = "windows_x86_64_gnullvm" 664 | version = "0.52.6" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 667 | 668 | [[package]] 669 | name = "windows_x86_64_msvc" 670 | version = "0.52.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 673 | 674 | [[package]] 675 | name = "xdg" 676 | version = "2.5.2" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 679 | 680 | [[package]] 681 | name = "zerocopy" 682 | version = "0.7.35" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 685 | dependencies = [ 686 | "byteorder", 687 | "zerocopy-derive", 688 | ] 689 | 690 | [[package]] 691 | name = "zerocopy-derive" 692 | version = "0.7.35" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 695 | dependencies = [ 696 | "proc-macro2", 697 | "quote", 698 | "syn", 699 | ] 700 | 701 | [[package]] 702 | name = "ziggy" 703 | version = "1.3.2" 704 | dependencies = [ 705 | "afl", 706 | "anyhow", 707 | "cargo_metadata", 708 | "clap", 709 | "console", 710 | "fork", 711 | "glob", 712 | "honggfuzz", 713 | "libc", 714 | "semver", 715 | "strip-ansi-escapes", 716 | "time-humanize", 717 | "twox-hash", 718 | ] 719 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ziggy" 3 | version = "1.3.2" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "A multi-fuzzer management utility for all of your Rust fuzzing needs 🧑‍🎤" 7 | repository = "https://github.com/srlabs/ziggy/" 8 | 9 | [workspace] 10 | members = [ 11 | ".", 12 | "examples/arbitrary", 13 | "examples/asan", 14 | "examples/url", 15 | ] 16 | 17 | [dependencies] 18 | afl = { version = "0.15.16", default-features = false, optional = true } 19 | anyhow = { version = "1.0.83", optional = true } 20 | cargo_metadata = { version = "0.18.1", optional = true } 21 | clap = { version = "4.5.4", features = ["cargo", "derive", "env"], optional = true } 22 | console = { version = "0.15.8", optional = true } 23 | fork = { version = "0.1.23", optional = true } 24 | glob = { version = "0.3.1", optional = true } 25 | honggfuzz = { version = "0.5.57", optional = true } 26 | libc = { version = "0.2.153", optional = true } 27 | semver = { version = "1.0.23", optional = true } 28 | strip-ansi-escapes = { version = "0.2.0", optional = true } 29 | time-humanize = { version = "0.1.3", optional = true } 30 | twox-hash = { version = "2.0.1", optional = true } 31 | 32 | [features] 33 | default = ["cli"] 34 | cli = [ 35 | "clap", 36 | "console", 37 | "glob", 38 | "semver", 39 | "anyhow", 40 | "strip-ansi-escapes", 41 | "libc", 42 | "time-humanize", 43 | "cargo_metadata", 44 | "twox-hash", 45 | ] 46 | coverage = ["fork", "libc"] 47 | 48 | [lints.clippy] 49 | needless_doctest_main = "allow" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `ziggy` 2 | 3 | `ziggy` is a fuzzer manager for Rust projects which is built to: 4 | 5 | - launch different fuzzers in parallel with a shared corpus 6 | - create and monitor continuous fuzzing pipelines 7 | 8 | ## Feature set 9 | 10 | - 🤹 handling of different fuzzing processes in parallel ([honggfuzz](https://github.com/google/honggfuzz), [AFL++](https://github.com/aflplusplus/aflplusplus)) 11 | - 🗃️ one shared corpus for all fuzzers 12 | - 🤏 effortless corpus minimization 13 | - 📊 insightful monitoring 14 | - 🎯 easy coverage report generation 15 | - 😶‍🌫️ Arbitrary trait support 16 | 17 | Features will also include: 18 | 19 | - 🐇 [LibAFL](https://github.com/aflplusplus/libafl) integration 20 | - 📨 notification of new crashes via bash hook 21 | 22 | ## Usage example 23 | 24 | First, install `ziggy` and its dependencies by running: 25 | 26 | ```bash 27 | cargo install --force ziggy cargo-afl honggfuzz grcov 28 | ``` 29 | 30 | Here is the output of the tool's help: 31 | 32 | ```text 33 | $ cargo ziggy 34 | A multi-fuzzer management utility for all of your Rust fuzzing needs 🧑‍🎤 35 | 36 | Usage: cargo ziggy 37 | 38 | Commands: 39 | build Build the fuzzer and the runner binaries 40 | fuzz Fuzz targets using different fuzzers in parallel 41 | run Run a specific input or a directory of inputs to analyze backtrace 42 | minimize Minimize the input corpus using the given fuzzing target 43 | cover Generate code coverage information using the existing corpus 44 | plot Plot AFL++ data using afl-plot 45 | add-seeds Add seeds to the running AFL++ fuzzers 46 | triage Triage crashes found with casr - currently only works for AFL++ 47 | help Print this message or the help of the given subcommand(s) 48 | 49 | Options: 50 | -h, --help Print help 51 | -V, --version Print version 52 | ``` 53 | 54 | To create a fuzzer, simply add `ziggy` as a dependency. 55 | 56 | ```toml 57 | [dependencies] 58 | ziggy = { version = "1.2", default-features = false } 59 | ``` 60 | 61 | Then use the `fuzz!` macro inside your `main` to create a harness. 62 | 63 | ```rust 64 | fn main() { 65 | ziggy::fuzz!(|data: &[u8]| { 66 | println!("{data:?}"); 67 | }); 68 | } 69 | ``` 70 | 71 | For a well-documented fuzzer, see [the url example](./examples/url/). 72 | 73 | ## The `output` directory 74 | 75 | After you've launched your fuzzer, you'll find a couple of items in the `output` directory: 76 | 77 | - the `corpus` directory containing the full corpus 78 | - the `crashes` directory containing any crashes detected by the fuzzers 79 | - the `logs` directory containing a fuzzer log files 80 | - the `afl` directory containing AFL++'s output 81 | - the `honggfuzz` directory containing Honggfuzz's output 82 | - the `queue` directory that is used by ziggy to pass items from AFL++ to Honggfuzz 83 | 84 | ## Note about coverage 85 | 86 | The `cargo cover` command will not generate coverage for the dependencies of your fuzzed project 87 | by default. 88 | 89 | If this is something you would like to change, you can use the following trick: 90 | ```bash 91 | CARGO_HOME=.cargo cargo ziggy cover 92 | ``` 93 | 94 | This will clone every dependency into a `.cargo` directory and this directory will be included in 95 | the generated coverage. 96 | 97 | ## `ziggy` logs 98 | 99 | If you want to see `ziggy`'s internal logs, you can set `RUST_LOG=INFO`. 100 | -------------------------------------------------------------------------------- /examples/arbitrary/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | Cargo.lock -------------------------------------------------------------------------------- /examples/arbitrary/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arbitrary-fuzz" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | ziggy = { path = "../../", default-features = false } 9 | arbitrary = { version = "1", features= ["derive"] } 10 | -------------------------------------------------------------------------------- /examples/arbitrary/README.md: -------------------------------------------------------------------------------- 1 | # Ziggy example - Arbitrary 2 | 3 | In the root project directory, run: 4 | 5 | ``` 6 | cargo install afl honggfuzz 7 | cargo install --force --path . 8 | ``` 9 | 10 | Then, in the `examples/arbitrary` directory, run: 11 | 12 | ``` 13 | cargo ziggy fuzz 14 | ``` 15 | -------------------------------------------------------------------------------- /examples/arbitrary/src/main.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Arbitrary; 2 | 3 | #[derive(Arbitrary, Debug, PartialEq, Eq)] 4 | pub struct Rgb { 5 | pub r: u8, 6 | pub g: u8, 7 | pub b: u8, 8 | } 9 | 10 | impl Rgb { 11 | #[must_use] 12 | pub fn as_hex(&self) -> Hex { 13 | let Rgb { r, g, b } = self; 14 | let panic_u8: u8 = 17; 15 | if r == &panic_u8 { 16 | panic!("let the fuzzer find this"); 17 | } 18 | Hex(format!("{:02X}{:02X}{:02X}", r, g, b)) 19 | } 20 | } 21 | 22 | pub struct Hex(String); 23 | 24 | impl Hex { 25 | fn as_rgb(&self) -> Rgb { 26 | let s = self.0.as_str(); 27 | 28 | let r = u8::from_str_radix(&s[..2], 16).unwrap(); 29 | let g = u8::from_str_radix(&s[2..4], 16).unwrap(); 30 | let b = u8::from_str_radix(&s[4..6], 16).unwrap(); 31 | 32 | Rgb { r, g, b } 33 | } 34 | } 35 | 36 | fn main() { 37 | ziggy::fuzz!(|color: Rgb| { 38 | let hex = color.as_hex(); 39 | let rgb = hex.as_rgb(); 40 | 41 | assert_eq!(color, rgb); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /examples/asan/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | Cargo.lock -------------------------------------------------------------------------------- /examples/asan/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asan-fuzz" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ziggy = { path = "../../", default-features = false } 8 | -------------------------------------------------------------------------------- /examples/asan/README.md: -------------------------------------------------------------------------------- 1 | # Ziggy example - URL 2 | 3 | First, install the tooling: 4 | 5 | ``` 6 | cargo install cargo-afl honggfuzz ziggy 7 | ``` 8 | 9 | ASAN mode is only available when using Rust Nightly. 10 | 11 | To fuzz, run in this directory: 12 | 13 | ``` 14 | cargo +nightly ziggy fuzz --asan 15 | ``` 16 | 17 | Note: 18 | The the runner must use ``--asan`` too! 19 | ``` 20 | cargo +nightly ziggy run --asan 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/asan/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | ziggy::fuzz!(|data: &[u8]| { 3 | if data.len() < 4 { 4 | return; 5 | } 6 | if data[0] == b'f' && data[1] == b'u' && data[2] == b'z' && data[3] == b'z' { 7 | let xs = [0, 1, 2, 3]; 8 | let _y = unsafe { *xs.as_ptr().offset(4) }; 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /examples/url/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | Cargo.lock -------------------------------------------------------------------------------- /examples/url/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "url-fuzz" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | url = "2.5.0" 9 | ziggy = { path = "../../", default-features = false } 10 | 11 | [features] 12 | fuzzing = [] -------------------------------------------------------------------------------- /examples/url/README.md: -------------------------------------------------------------------------------- 1 | # Ziggy example - URL 2 | 3 | First, install the tooling: 4 | 5 | ``` 6 | cargo install cargo-afl honggfuzz ziggy 7 | ``` 8 | 9 | Then, in this directory, run: 10 | 11 | ``` 12 | cargo ziggy fuzz 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/url/src/main.rs: -------------------------------------------------------------------------------- 1 | // This document outlines different ways of fuzzing a library: 2 | // - Invariant Fuzzing 3 | // - Differential Fuzzing 4 | // - Correctness Fuzzing 5 | // - Consistency Fuzzing 6 | // - Idempotency Fuzzing 7 | 8 | // Invariant Fuzzing 9 | // We run the program, and check the correctness of important values. 10 | // For example, we could check that a process is always successful by calling `.unwrap()`, or we 11 | // could assert that a certain value satisfies a property. 12 | fn invariant_fuzz(data: &str) { 13 | if let Ok(parsed) = url::Url::parse(data) { 14 | #[cfg(not(feature = "fuzzing"))] 15 | println!("{data} => {parsed}"); 16 | // We assert that the string representation of the URL always contains a ':' 17 | // character. 18 | assert!(parsed.to_string().contains(':')); 19 | } 20 | } 21 | 22 | // Differential Fuzzing 23 | // We run two implementations of the same process on identical data and verify that the outputs 24 | // match. 25 | // This requires two different implementations of the same process, and can catch bugs in both. 26 | fn differential_fuzz(data: &str) { 27 | // We do not have an alternative implementation, so we mock one. 28 | let other_parse = url::Url::parse; 29 | // We run both `parse` methods and assert the results are equal. 30 | assert_eq!(url::Url::parse(data), other_parse(data)); 31 | } 32 | 33 | // Correctness Fuzzing 34 | // We run a linter on data, as well as on the same data that has gone through a formatter, and 35 | // verify the outputs are the same. 36 | // In doing this, we check the validity of both the linter and the formatter. 37 | // Here, we have created an imperfect example as we are not using actual linters and formatters. A 38 | // correct example would be using `cargo fmt` as a formatter and `cargo clippy` as a linter on a 39 | // piece of Rust code. 40 | fn correctness_fuzz(data: &str) { 41 | // We use the URL parser as a linter. 42 | if let Ok(linted) = url::Url::parse(data) { 43 | // We use `trim()` as a formatter. 44 | // This formatter removes any leading whitespaces from the input string. 45 | // In theory, this should not have any impact on the URL. We test that this is the case. 46 | let formatted_data = data.trim_start(); 47 | let linted_formatted = 48 | url::Url::parse(formatted_data).expect("formatted data should still be lintable"); 49 | assert_eq!(linted, linted_formatted); 50 | } 51 | } 52 | 53 | // Consistency Fuzzing 54 | // We run an encoder on an input, then a decoder on that output, and verify that the final value 55 | // is the same as the first input. This verifies the consistency of an encode/decode pair. 56 | fn consistency_fuzz(data: &str) { 57 | // The input is a single character. 58 | if let Some(input) = data.chars().next() { 59 | let mut output = [0; 2]; 60 | let _ = input.encode_utf16(&mut output); 61 | let result = char::decode_utf16(output) 62 | .next() 63 | .expect("decoded value should contain one character") 64 | .expect("character should be properly decoded after it has been encoded"); 65 | assert_eq!(input, result); 66 | } 67 | } 68 | 69 | // Idempotency Fuzzing 70 | // We run sequentially a parser, an unparser, a parser and an unparser, and assert that both 71 | // outputs from the unparsers are equal. This verifies that the parser/unparser pair is 72 | // idempotent. 73 | // https://en.wikipedia.org/wiki/Idempotence 74 | // Here, the pair of operations is str::as_bytes(&self) and str::from_utf8(&[u8]). 75 | fn idempotency_fuzz(data: &str) { 76 | // We have already parsed the data once in the main harness. 77 | let parsed_once = data; 78 | let unparsed_once = parsed_once.as_bytes(); 79 | let parsed_twice = std::str::from_utf8(unparsed_once) 80 | .expect("data should be parseable a second time after being unparsed"); 81 | let unparsed_twice = parsed_twice.as_bytes(); 82 | assert_eq!(unparsed_once, unparsed_twice); 83 | } 84 | 85 | fn main() { 86 | ziggy::fuzz!(|data: &[u8]| { 87 | if let Ok(string) = std::str::from_utf8(data) { 88 | invariant_fuzz(string); 89 | differential_fuzz(string); 90 | correctness_fuzz(string); 91 | consistency_fuzz(string); 92 | idempotency_fuzz(string); 93 | } 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /examples/url/url.dict: -------------------------------------------------------------------------------- 1 | "\xc7" 2 | "https://hello.com/?hi=32&hello=world" 3 | "https://hello.com/?firstname=Geeks%20for%20&lastname=Geeks" 4 | "%23first=%25try%25" -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/add_seeds.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::{env, process}; 3 | 4 | impl AddSeeds { 5 | pub fn add_seeds(&mut self) -> Result<(), anyhow::Error> { 6 | eprintln!("Adding seeds to AFL"); 7 | 8 | let req = semver::VersionReq::parse(">=0.14.5").unwrap(); 9 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 10 | let afl_version = process::Command::new(cargo) 11 | .args(["afl", "--version"]) 12 | .output() 13 | .context("could not run `cargo afl --version`")?; 14 | 15 | if !std::str::from_utf8(afl_version.stdout.as_slice()) 16 | .unwrap_or_default() 17 | .split_whitespace() 18 | .nth(1) 19 | .context("could not get afl version from stdout") 20 | .map(semver::Version::parse) 21 | .context("could not parse cargo-afl version")? 22 | .map(|v| req.matches(&v))? 23 | { 24 | return Err(anyhow!("Outdated version of cargo-afl, ziggy needs >=0.14.5, please run `cargo install cargo-afl`")); 25 | } 26 | 27 | self.target = find_target(&self.target)?; 28 | 29 | let input = self 30 | .input 31 | .display() 32 | .to_string() 33 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 34 | .replace("{target_name}", &self.target); 35 | 36 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 37 | process::Command::new(cargo.clone()) 38 | .args( 39 | [ 40 | "afl", 41 | "addseeds", 42 | "-o", 43 | &format!("{}/{}/afl", self.ziggy_output.display(), self.target), 44 | "-i", 45 | &input, 46 | ] 47 | .iter() 48 | .filter(|a| a != &&""), 49 | ) 50 | .spawn()? 51 | .wait()?; 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/build.rs: -------------------------------------------------------------------------------- 1 | use crate::Build; 2 | use anyhow::{anyhow, Context, Result}; 3 | use console::style; 4 | use std::{env, process}; 5 | 6 | /// Target for ASAN builds 7 | /// Note: we need to supply a target due to -Z build-std 8 | /// Note: we need to use -Z build-std or else many macros cannot be built when using ASAN 9 | pub const ASAN_TARGET: &str = "x86_64-unknown-linux-gnu"; 10 | 11 | impl Build { 12 | /// Build the fuzzers 13 | pub fn build(&self) -> Result<(), anyhow::Error> { 14 | // No fuzzers for you 15 | if self.no_afl && self.no_honggfuzz { 16 | return Err(anyhow!("Pick at least one fuzzer")); 17 | } 18 | 19 | // The cargo executable 20 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 21 | 22 | if !self.no_afl { 23 | eprintln!(" {} afl", style("Building").red().bold()); 24 | let mut afl_args = vec![ 25 | "afl", 26 | "build", 27 | "--features=ziggy/afl", 28 | "--target-dir=target/afl", 29 | ]; 30 | 31 | // Add the --release argument if self.release is true 32 | if self.release { 33 | assert!(!self.asan, "cannot use --release for ASAN builds"); 34 | afl_args.push("--release"); 35 | } 36 | 37 | let opt_level = env::var("AFL_OPT_LEVEL").unwrap_or("0".to_string()); 38 | let mut rust_flags = env::var("RUSTFLAGS").unwrap_or_default(); 39 | let mut rust_doc_flags = env::var("RUSTDOCFLAGS").unwrap_or_default(); 40 | 41 | // First fuzzer we build: AFL++ 42 | let run = process::Command::new(cargo.clone()) 43 | .args(&afl_args) 44 | .env("AFL_QUIET", "1") 45 | // need to specify for afl.rs so that we build with -Copt-level=0 46 | .env("AFL_OPT_LEVEL", &opt_level) 47 | .env("AFL_LLVM_CMPLOG", "1") // for afl.rs feature "plugins" 48 | .env("RUSTFLAGS", &rust_flags) 49 | .env("RUSTDOCFLAGS", &rust_doc_flags) 50 | .spawn()? 51 | .wait() 52 | .context("Error spawning afl build command")?; 53 | 54 | if !run.success() { 55 | return Err(anyhow!( 56 | "Error building afl fuzzer: Exited with {:?}", 57 | run.code() 58 | )); 59 | } 60 | 61 | let asan_target_str = format!("--target={ASAN_TARGET}"); 62 | let opt_level_str = format!("-Copt-level={opt_level}"); 63 | 64 | // If ASAN is enabled, build both a sanitized binary and a non-sanitized binary. 65 | if self.asan { 66 | eprintln!(" {} afl (ASan)", style("Building").red().bold()); 67 | assert_eq!(opt_level, "0", "AFL_OPT_LEVEL must be 0 for ASAN builds"); 68 | afl_args.push(&asan_target_str); 69 | afl_args.extend(["-Z", "build-std"]); 70 | rust_flags.push_str(" -Zsanitizer=address "); 71 | rust_flags.push_str(&opt_level_str); 72 | rust_doc_flags.push_str(" -Zsanitizer=address "); 73 | 74 | let run = process::Command::new(cargo.clone()) 75 | .args(afl_args) 76 | .env("AFL_QUIET", "1") 77 | // need to specify for afl.rs so that we build with -Copt-level=0 78 | .env("AFL_OPT_LEVEL", opt_level) 79 | .env("AFL_LLVM_CMPLOG", "1") // for afl.rs feature "plugins" 80 | .env("RUSTFLAGS", rust_flags) 81 | .env("RUSTDOCFLAGS", rust_doc_flags) 82 | .spawn()? 83 | .wait() 84 | .context("Error spawning afl build command")?; 85 | 86 | if !run.success() { 87 | return Err(anyhow!( 88 | "Error building afl fuzzer: Exited with {:?}", 89 | run.code() 90 | )); 91 | } 92 | }; 93 | 94 | eprintln!(" {} afl", style("Finished").cyan().bold()); 95 | } 96 | 97 | if !self.no_honggfuzz { 98 | assert!( 99 | !self.asan, 100 | "Cannot build honggfuzz with ASAN for the moment. use --no-honggfuzz" 101 | ); 102 | eprintln!(" {} honggfuzz", style("Building").red().bold()); 103 | 104 | let mut hfuzz_args = vec!["hfuzz", "build"]; 105 | 106 | // Add the --release argument if self.release is true 107 | if self.release { 108 | hfuzz_args.push("--release"); 109 | } 110 | 111 | // Second fuzzer we build: Honggfuzz 112 | let run = process::Command::new(cargo) 113 | .args(hfuzz_args) 114 | .env("CARGO_TARGET_DIR", "./target/honggfuzz") 115 | .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") 116 | .env("RUSTFLAGS", env::var("RUSTFLAGS").unwrap_or_default()) 117 | .stdout(process::Stdio::piped()) 118 | .spawn()? 119 | .wait() 120 | .context("Error spawning hfuzz build command")?; 121 | 122 | if !run.success() { 123 | return Err(anyhow!( 124 | "Error building honggfuzz fuzzer: Exited with {:?}", 125 | run.code() 126 | )); 127 | } 128 | 129 | eprintln!(" {} honggfuzz", style("Finished").cyan().bold()); 130 | } 131 | 132 | if std::env::var("AFL_LLVM_CMPGLOG").is_ok() { 133 | panic!( 134 | "Even the mighty may fall, especially on 77b2c27a59bb858045c4db442989ce8f20c8ee11" 135 | ) 136 | } 137 | 138 | Ok(()) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/coverage.rs: -------------------------------------------------------------------------------- 1 | use crate::{find_target, Cover}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use glob::glob; 4 | use std::{env, fs, path::PathBuf, process}; 5 | 6 | impl Cover { 7 | pub fn generate_coverage(&mut self) -> Result<(), anyhow::Error> { 8 | process::Command::new("grcov") 9 | .arg("--version") 10 | .output() 11 | .context("grcov not found - please install by running `cargo install grcov`")?; 12 | 13 | eprintln!("Generating coverage"); 14 | 15 | self.target = 16 | find_target(&self.target).context("⚠️ couldn't find the target to start coverage")?; 17 | 18 | if let Some(path) = &self.source { 19 | if !path.try_exists()? { 20 | return Err(anyhow!( 21 | "Source directory specified, but path does not exist!" 22 | )); 23 | } 24 | } 25 | 26 | // build the runner 27 | Cover::build_runner()?; 28 | 29 | if !self.keep { 30 | // We remove the previous coverage files 31 | Cover::clean_old_cov()?; 32 | } 33 | 34 | let mut shared_corpus = PathBuf::new(); 35 | 36 | shared_corpus.push( 37 | self.input 38 | .display() 39 | .to_string() 40 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 41 | .replace("{target_name}", &self.target) 42 | .as_str(), 43 | ); 44 | 45 | let _ = process::Command::new(format!("./target/coverage/debug/{}", &self.target)) 46 | .arg(format!("{}", shared_corpus.display())) 47 | .env( 48 | "LLVM_PROFILE_FILE", 49 | "target/coverage/debug/deps/coverage-%p-%m.profraw", 50 | ) 51 | .spawn() 52 | .unwrap() 53 | .wait_with_output() 54 | .unwrap(); 55 | 56 | let source_or_workspace_root = match &self.source { 57 | Some(s) => s.display().to_string(), 58 | None => { 59 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 60 | metadata.workspace_root.into() 61 | } 62 | }; 63 | 64 | let coverage_dir = self 65 | .output 66 | .display() 67 | .to_string() 68 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 69 | .replace("{target_name}", &self.target); 70 | 71 | // We remove the previous coverage 72 | if let Err(error) = fs::remove_dir_all(&coverage_dir) { 73 | match error.kind() { 74 | std::io::ErrorKind::NotFound => {} 75 | e => return Err(anyhow!(e)), 76 | } 77 | }; 78 | 79 | let output_types = match &self.output_types { 80 | Some(o) => o, 81 | None => "html", 82 | }; 83 | 84 | // We generate the code coverage report 85 | Cover::run_grcov( 86 | &self.target, 87 | output_types, 88 | &coverage_dir, 89 | &source_or_workspace_root, 90 | ) 91 | } 92 | 93 | /// Build the runner with the appropriate flags for coverage 94 | pub fn build_runner() -> Result<(), anyhow::Error> { 95 | // The cargo executable 96 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 97 | 98 | let mut coverage_rustflags = env::var("COVERAGE_RUSTFLAGS") 99 | .unwrap_or_else(|_| String::from("-Cinstrument-coverage")); 100 | coverage_rustflags.push_str(&env::var("RUSTFLAGS").unwrap_or_default()); 101 | 102 | let build = process::Command::new(cargo) 103 | .args([ 104 | "rustc", 105 | "--target-dir=target/coverage", 106 | "--features=ziggy/coverage", 107 | ]) 108 | .env("RUSTFLAGS", coverage_rustflags) 109 | .spawn() 110 | .context("⚠️ couldn't spawn rustc for coverage")? 111 | .wait() 112 | .context("⚠️ couldn't wait for the rustc during coverage")?; 113 | if !build.success() { 114 | return Err(anyhow!("⚠️ build failed")); 115 | } 116 | Ok(()) 117 | } 118 | 119 | pub fn run_grcov( 120 | target: &str, 121 | output_types: &str, 122 | coverage_dir: &str, 123 | source_or_workspace_root: &str, 124 | ) -> Result<(), anyhow::Error> { 125 | process::Command::new("grcov") 126 | .args([ 127 | ".", 128 | &format!("-b=./target/coverage/debug/{}", target), 129 | &format!("-s={source_or_workspace_root}"), 130 | &format!("-t={}", output_types), 131 | "--llvm", 132 | "--branch", 133 | "--ignore-not-existing", 134 | &format!("-o={coverage_dir}"), 135 | ]) 136 | .spawn() 137 | .context("⚠️ cannot find grcov in your path, please install it")? 138 | .wait() 139 | .context("⚠️ couldn't wait for the grcov process")?; 140 | Ok(()) 141 | } 142 | 143 | pub fn clean_old_cov() -> Result<(), anyhow::Error> { 144 | if let Ok(profile_files) = glob("target/coverage/debug/deps/*.profraw") { 145 | for file in profile_files.flatten() { 146 | let file_string = &file.display(); 147 | fs::remove_file(&file).context(format!("⚠️ couldn't remove {}", file_string))?; 148 | } 149 | } 150 | Ok(()) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/fuzz.rs: -------------------------------------------------------------------------------- 1 | use crate::build::ASAN_TARGET; 2 | use crate::*; 3 | use anyhow::{anyhow, Error}; 4 | use console::{style, Term}; 5 | use glob::glob; 6 | use std::{ 7 | env, 8 | fs::File, 9 | io::Write, 10 | path::Path, 11 | process::{self, Stdio}, 12 | sync::{Arc, Mutex}, 13 | thread, 14 | time::{Duration, Instant, SystemTime, UNIX_EPOCH}, 15 | }; 16 | use strip_ansi_escapes::strip_str; 17 | use twox_hash::XxHash64; 18 | 19 | /// Main logic for managing fuzzers and the fuzzing process in ziggy. 20 | /// ## Initial minimization logic 21 | /// When launching fuzzers, if initial corpora exist, they are merged together and we minimize it 22 | /// with both AFL++ and Honggfuzz. 23 | /// ```text 24 | /// # bash pseudocode 25 | /// cp all_afl_corpora/* corpus/* corpus_tmp/ 26 | /// # run afl++ minimization 27 | /// afl++_minimization -i corpus_tmp -o corpus_minimized 28 | /// # in parallel, run honggfuzz minimization 29 | /// honggfuzz_minimization -i corpus_tmp -o corpus_minimized 30 | /// rm -rf corpus corpus_tmp 31 | /// mv corpus_minimized corpus 32 | /// afl++ -i corpus -o all_afl_corpora & 33 | /// honggfuzz -i corpus -o corpus 34 | /// ``` 35 | /// The `all_afl_corpora` directory corresponds to the `output/target_name/afl/**/queue/` directories. 36 | impl Fuzz { 37 | pub fn corpus(&self) -> String { 38 | self.corpus 39 | .display() 40 | .to_string() 41 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 42 | .replace("{target_name}", &self.target) 43 | } 44 | 45 | pub fn corpus_tmp(&self) -> String { 46 | format!("{}/corpus_tmp/", self.output_target()) 47 | } 48 | 49 | pub fn corpus_minimized(&self) -> String { 50 | format!("{}/corpus_minimized/", self.output_target(),) 51 | } 52 | 53 | pub fn output_target(&self) -> String { 54 | format!("{}/{}", self.ziggy_output.display(), self.target) 55 | } 56 | 57 | /// Returns true if AFL++ is enabled 58 | pub fn afl(&self) -> bool { 59 | !self.no_afl 60 | } 61 | 62 | /// Returns true if Honggfuzz is enabled 63 | // This definition could be a one-liner but it was expanded for clarity 64 | pub fn honggfuzz(&self) -> bool { 65 | if self.fuzz_binary() { 66 | // We cannot use honggfuzz in binary mode 67 | false 68 | } else if self.no_afl { 69 | // If we have "no_afl" set then honggfuzz is always enabled 70 | true 71 | } else { 72 | // If honggfuzz is not disabled, we use it if there are more than 1 jobs 73 | !self.no_honggfuzz && self.jobs > 1 74 | } 75 | } 76 | 77 | fn fuzz_binary(&self) -> bool { 78 | self.binary.is_some() 79 | } 80 | 81 | // Manages the continuous running of fuzzers 82 | pub fn fuzz(&mut self) -> Result<(), anyhow::Error> { 83 | if !self.fuzz_binary() { 84 | let build = Build { 85 | no_afl: !self.afl(), 86 | no_honggfuzz: !self.honggfuzz(), 87 | release: self.release, 88 | asan: self.asan, 89 | }; 90 | build.build().context("Failed to build the fuzzers")?; 91 | } 92 | 93 | self.target = if self.fuzz_binary() { 94 | self.binary 95 | .as_ref() 96 | .expect("invariant; should never occur") 97 | .display() 98 | .to_string() 99 | } else { 100 | find_target(&self.target).context("⚠️ couldn't find target when fuzzing")? 101 | }; 102 | 103 | let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(); 104 | 105 | let crash_dir = format!("{}/crashes/{}/", self.output_target(), time); 106 | let crash_path = Path::new(&crash_dir); 107 | fs::create_dir_all(crash_path)?; 108 | 109 | let _ = process::Command::new("mkdir") 110 | .args([ 111 | "-p", 112 | &format!("{}/logs/", self.output_target()), 113 | &format!("{}/queue/", self.output_target()), 114 | ]) 115 | .stderr(process::Stdio::piped()) 116 | .spawn()? 117 | .wait()?; 118 | 119 | if Path::new(&self.corpus()).exists() { 120 | if self.minimize { 121 | fs::create_dir_all(self.corpus_tmp()) 122 | .context("Could not create temporary corpus")?; 123 | self.copy_corpora() 124 | .context("Could not move all seeds to temporary corpus")?; 125 | let _ = fs::remove_dir_all(self.corpus_minimized()); 126 | self.run_minimization() 127 | .context("Failure while minimizing")?; 128 | fs::remove_dir_all(self.corpus()).context("Could not remove shared corpus")?; 129 | fs::rename(self.corpus_minimized(), self.corpus()) 130 | .context("Could not move minimized corpus over")?; 131 | fs::remove_dir_all(self.corpus_tmp()) 132 | .context("Could not remove temporary corpus")?; 133 | } 134 | } else { 135 | let _ = process::Command::new("mkdir") 136 | .args(["-p", &self.corpus()]) 137 | .stderr(process::Stdio::piped()) 138 | .spawn()? 139 | .wait()?; 140 | } 141 | 142 | // We create an initial corpus file, so that AFL++ starts-up properly if corpus is empty 143 | let is_empty = fs::read_dir(self.corpus())?.next().is_none(); // check if corpus has some seeds 144 | if is_empty { 145 | let mut initial_corpus = File::create(self.corpus() + "/init")?; 146 | writeln!(&mut initial_corpus, "00000000")?; 147 | drop(initial_corpus); 148 | } 149 | 150 | let mut processes = self.spawn_new_fuzzers()?; 151 | 152 | self.start_time = Instant::now(); 153 | 154 | let mut last_synced_created_time: Option = None; 155 | let mut last_sync_time = Instant::now(); 156 | let mut afl_output_ok = false; 157 | 158 | if self.no_afl && self.coverage_worker { 159 | return Err(anyhow!("cannot use --no-afl with --coverage-worker!")); 160 | } 161 | 162 | // We prepare builds for the coverage worker 163 | if self.coverage_worker { 164 | Cover::clean_old_cov()?; 165 | Cover::build_runner()?; 166 | } 167 | let cov_start_time = Arc::new(Mutex::new(None)); 168 | let cov_end_time = Arc::new(Mutex::new(Instant::now())); 169 | let coverage_now_running = Arc::new(Mutex::new(false)); 170 | let workspace_root = cargo_metadata::MetadataCommand::new() 171 | .exec()? 172 | .workspace_root 173 | .to_string(); 174 | let target = self.target.clone(); 175 | let main_corpus = self.corpus(); 176 | let output_target = self.output_target(); 177 | 178 | loop { 179 | let sleep_duration = Duration::from_secs(1); 180 | thread::sleep(sleep_duration); 181 | 182 | let coverage_status = match ( 183 | self.coverage_worker, 184 | *coverage_now_running.lock().unwrap(), 185 | cov_end_time.lock().unwrap().elapsed().as_secs() / 60, 186 | ) { 187 | (true, false, wait) if wait < self.coverage_interval => { 188 | format!("waiting {} minutes", self.coverage_interval - wait) 189 | } 190 | (true, false, _) => String::from("starting"), 191 | (true, true, _) => String::from("running"), 192 | (false, _, _) => String::from("disabled"), 193 | }; 194 | 195 | self.print_stats(&coverage_status); 196 | 197 | if coverage_status.as_str() == "starting" { 198 | *coverage_now_running.lock().unwrap() = true; 199 | 200 | let main_corpus = main_corpus.clone(); 201 | let target = target.clone(); 202 | let workspace_root = workspace_root.clone(); 203 | let output_target = output_target.clone(); 204 | let cov_start_time = Arc::clone(&cov_start_time); 205 | let cov_end_time = Arc::clone(&cov_end_time); 206 | let coverage_now_running = Arc::clone(&coverage_now_running); 207 | 208 | thread::spawn(move || { 209 | let mut seen_new_entry = false; 210 | let prev_start_time = { 211 | let unlocked = cov_start_time.lock().unwrap(); 212 | *unlocked 213 | }; 214 | *cov_start_time.lock().unwrap() = Some(Instant::now()); 215 | let entries = std::fs::read_dir(&main_corpus).unwrap(); 216 | for entry in entries.flatten().map(|e| e.path()) { 217 | // We only want to run corpus entries created since the last time we ran. 218 | let created = entry 219 | .metadata() 220 | .unwrap() 221 | .created() 222 | .unwrap() 223 | .elapsed() 224 | .unwrap_or_default(); 225 | if prev_start_time 226 | .map(|s| s.elapsed()) 227 | .unwrap_or(Duration::MAX) 228 | >= created 229 | { 230 | let _ = process::Command::new(format!( 231 | "./target/coverage/debug/{}", 232 | &target 233 | )) 234 | .arg(format!("{}", entry.display())) 235 | .stdout(Stdio::null()) 236 | .stderr(Stdio::null()) 237 | .status(); 238 | seen_new_entry = true; 239 | } 240 | } 241 | if seen_new_entry { 242 | let coverage_dir = output_target + "/coverage"; 243 | let _ = fs::remove_dir_all(&coverage_dir); 244 | Cover::run_grcov(&target.clone(), "html", &coverage_dir, &workspace_root) 245 | .unwrap(); 246 | } 247 | 248 | *cov_end_time.lock().unwrap() = Instant::now(); 249 | *coverage_now_running.lock().unwrap() = false; 250 | }); 251 | } 252 | 253 | if !afl_output_ok { 254 | if let Ok(afl_log) = 255 | fs::read_to_string(format!("{}/logs/afl.log", self.output_target())) 256 | { 257 | if afl_log.contains("ready to roll") { 258 | afl_output_ok = true; 259 | } else if afl_log.contains("echo core >/proc/sys/kernel/core_pattern") 260 | || afl_log.contains("cd /sys/devices/system/cpu") 261 | { 262 | stop_fuzzers(&mut processes)?; 263 | eprintln!("We highly recommend you configure your system for better performance:\n"); 264 | eprintln!(" cargo afl system-config\n"); 265 | eprintln!( 266 | "Or set AFL_SKIP_CPUFREQ and AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES\n" 267 | ); 268 | return Ok(()); 269 | } 270 | } 271 | } 272 | 273 | // We check AFL++ and Honggfuzz's outputs for crash files and copy them over to 274 | // our own crashes directory 275 | let crash_dirs = glob(&format!("{}/afl/*/crashes", self.output_target())) 276 | .map_err(|_| anyhow!("Failed to read crashes glob pattern"))? 277 | .flatten() 278 | .chain(vec![format!( 279 | "{}/honggfuzz/{}", 280 | self.output_target(), 281 | self.target 282 | ) 283 | .into()]); 284 | 285 | for crash_dir in crash_dirs { 286 | if let Ok(crashes) = fs::read_dir(crash_dir) { 287 | for crash_input in crashes.flatten() { 288 | let file_name = crash_input.file_name(); 289 | let to_path = crash_path.join(&file_name); 290 | if to_path.exists() 291 | || ["", "README.txt", "HONGGFUZZ.REPORT.TXT", "input"] 292 | .contains(&file_name.to_str().unwrap_or_default()) 293 | { 294 | continue; 295 | } 296 | fs::copy(crash_input.path(), to_path)?; 297 | } 298 | } 299 | } 300 | 301 | // If both fuzzers are running, we copy over AFL++'s queue for consumption by Honggfuzz. 302 | // We also copy-over each live corpus to the shared corpus directory, where each file 303 | // name is the md5 hash of the file. This happens every 10 minutes. 304 | if last_sync_time.elapsed().as_secs() > 10 * 60 { 305 | let mut files = vec![]; 306 | if self.afl() { 307 | files.append( 308 | &mut glob(&format!( 309 | "{}/afl/mainaflfuzzer/queue/*", 310 | self.output_target(), 311 | ))? 312 | .flatten() 313 | .collect(), 314 | ); 315 | } 316 | if self.honggfuzz() { 317 | files.append( 318 | &mut glob(&format!("{}/honggfuzz/corpus/*", self.output_target(),))? 319 | .flatten() 320 | .collect(), 321 | ); 322 | } 323 | let mut newest_time = last_synced_created_time; 324 | let valid_files = files.iter().filter(|file| { 325 | if let Ok(metadata) = file.metadata() { 326 | let created = metadata.created().unwrap(); 327 | if last_synced_created_time.is_none_or(|time| created > time) { 328 | if newest_time.is_none_or(|time| created > time) { 329 | newest_time = Some(created); 330 | } 331 | return true; 332 | } 333 | } 334 | false 335 | }); 336 | for file in valid_files { 337 | if let Some(file_name) = file.file_name() { 338 | if self.honggfuzz() { 339 | let _ = fs::copy( 340 | file, 341 | format!("{}/queue/{:?}", self.output_target(), file_name), 342 | ); 343 | } 344 | // Hash the file to get its file name 345 | let bytes = fs::read(file).unwrap_or_default(); 346 | let hash = XxHash64::oneshot(0, &bytes); 347 | let _ = fs::copy(file, format!("{}/corpus/{hash:x}", self.output_target())); 348 | } 349 | } 350 | last_synced_created_time = newest_time; 351 | last_sync_time = Instant::now(); 352 | } 353 | 354 | if processes 355 | .iter_mut() 356 | .all(|p| p.try_wait().unwrap_or(None).is_some()) 357 | { 358 | stop_fuzzers(&mut processes)?; 359 | return Ok(()); 360 | } 361 | } 362 | } 363 | 364 | // Spawns new fuzzers 365 | pub fn spawn_new_fuzzers(&self) -> Result, anyhow::Error> { 366 | // No fuzzers for you 367 | if self.no_afl && self.no_honggfuzz { 368 | return Err(anyhow!( 369 | "Pick at least one fuzzer.\nNote: -b/--binary implies --no-honggfuzz" 370 | )); 371 | } 372 | 373 | let mut fuzzer_handles = vec![]; 374 | 375 | // The cargo executable 376 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 377 | 378 | let (afl_jobs, honggfuzz_jobs) = { 379 | if self.no_afl { 380 | (0, self.jobs) 381 | } else if self.no_honggfuzz || self.fuzz_binary() { 382 | (self.jobs, 0) 383 | } else { 384 | // we assign roughly 2/3 to AFL++, 1/3 to honggfuzz, however do 385 | // not apply more than 4 jobs to honggfuzz 386 | match self.jobs { 387 | 1 => (1, 0), 388 | 2..=12 => (self.jobs - self.jobs.div_ceil(3), self.jobs.div_ceil(3)), 389 | _ => (self.jobs - 4, 4), 390 | } 391 | } 392 | }; 393 | 394 | if honggfuzz_jobs > 4 { 395 | eprintln!("Warning: running more honggfuzz jobs than 4 is not effective"); 396 | } 397 | 398 | if afl_jobs > 0 { 399 | let _ = process::Command::new("mkdir") 400 | .args(["-p", &format!("{}/afl", self.output_target())]) 401 | .stderr(process::Stdio::piped()) 402 | .spawn()? 403 | .wait()?; 404 | 405 | // https://aflplus.plus/docs/fuzzing_in_depth/#c-using-multiple-cores 406 | let afl_modes = [ 407 | "explore", "fast", "coe", "lin", "quad", "exploit", "rare", "explore", "fast", 408 | "mmopt", 409 | ]; 410 | 411 | for job_num in 0..afl_jobs { 412 | let is_main_instance = job_num == 0; 413 | // We set the fuzzer name, and if it's the main or a secondary fuzzer 414 | let fuzzer_name = match is_main_instance { 415 | true => String::from("-Mmainaflfuzzer"), 416 | false => format!("-Ssecondaryfuzzer{job_num}"), 417 | }; 418 | // We only sync to the shared corpus if Honggfuzz is also running 419 | let use_shared_corpus = match (self.no_honggfuzz, job_num) { 420 | (false, 0) => format!("-F{}", &self.corpus()), 421 | _ => String::new(), 422 | }; 423 | let use_initial_corpus_dir = match (&self.initial_corpus, job_num) { 424 | (Some(initial_corpus), 0) => { 425 | format!("-F{}", &initial_corpus.display().to_string()) 426 | } 427 | _ => String::new(), 428 | }; 429 | // 10% of secondary fuzzers have the MOpt mutator enabled 430 | let mopt_mutator = match job_num % 10 { 431 | 9 => "-L0", 432 | _ => "", 433 | }; 434 | // Power schedule 435 | let power_schedule = afl_modes 436 | .get(job_num as usize % afl_modes.len()) 437 | .unwrap_or(&"fast"); 438 | // Old queue cycling 439 | let old_queue_cycling = match job_num % 10 { 440 | 8 => "-Z", 441 | _ => "", 442 | }; 443 | // Only few instances do cmplog 444 | let cmplog_options = match job_num { 445 | 1 => "-l2a", 446 | 3 => "-l1", 447 | 14 => "-l2a", 448 | 22 => "-l3at", 449 | _ => "-c-", // disable Cmplog, needs AFL++ 4.08a 450 | }; 451 | // AFL timeout is in ms so we convert the value 452 | let timeout_option_afl = match self.timeout { 453 | Some(t) => format!("-t{}", t * 1000), 454 | None => String::new(), 455 | }; 456 | let memory_option_afl = match &self.memory_limit { 457 | Some(m) => format!("-m{}", m), 458 | None => String::new(), 459 | }; 460 | let dictionary_option = match &self.dictionary { 461 | Some(d) => format!("-x{}", &d.display().to_string()), 462 | None => String::new(), 463 | }; 464 | let mutation_option = match job_num / 5 { 465 | 0..=1 => "-P600", 466 | 2..=3 => "-Pexplore", 467 | _ => "-Pexploit", 468 | }; 469 | let input_format_option = self.config.input_format_flag(); 470 | let log_destination = || match job_num { 471 | 0 => File::create(format!("{}/logs/afl.log", self.output_target())) 472 | .unwrap() 473 | .into(), 474 | 1 => File::create(format!("{}/logs/afl_1.log", self.output_target())) 475 | .unwrap() 476 | .into(), 477 | _ => process::Stdio::null(), 478 | }; 479 | let final_sync = match job_num { 480 | 0 => "AFL_FINAL_SYNC", 481 | _ => "_DUMMY_VAR", 482 | }; 483 | let target_path = match self.fuzz_binary() { 484 | true => self.target.clone(), 485 | false => { 486 | if self.release { 487 | format!("./target/afl/release/{}", self.target) 488 | } else if self.asan && job_num == 0 { 489 | format!("./target/afl/{ASAN_TARGET}/debug/{}", self.target) 490 | } else { 491 | format!("./target/afl/debug/{}", self.target) 492 | } 493 | } 494 | }; 495 | 496 | let mut afl_flags = self.afl_flags.clone(); 497 | if is_main_instance { 498 | for path in &self.foreign_sync_dirs { 499 | afl_flags.push(format!("-F {}", path.display())) 500 | } 501 | } 502 | 503 | fuzzer_handles.push( 504 | process::Command::new(cargo.clone()) 505 | .args( 506 | [ 507 | "afl", 508 | "fuzz", 509 | &fuzzer_name, 510 | &format!("-i{}", self.corpus()), 511 | &format!("-p{power_schedule}"), 512 | &format!("-o{}/afl", self.output_target()), 513 | &format!("-g{}", self.min_length), 514 | &format!("-G{}", self.max_length), 515 | &use_shared_corpus, 516 | &use_initial_corpus_dir, 517 | old_queue_cycling, 518 | cmplog_options, 519 | mopt_mutator, 520 | mutation_option, 521 | input_format_option, 522 | &timeout_option_afl, 523 | &memory_option_afl, 524 | &dictionary_option, 525 | ] 526 | .iter() 527 | .filter(|a| a != &&""), 528 | ) 529 | .args(afl_flags) 530 | .arg(target_path) 531 | .env("AFL_AUTORESUME", "1") 532 | .env("AFL_TESTCACHE_SIZE", "100") 533 | .env("AFL_FAST_CAL", "1") 534 | .env("AFL_FORCE_UI", "1") 535 | .env("AFL_IGNORE_UNKNOWN_ENVS", "1") 536 | .env("AFL_CMPLOG_ONLY_NEW", "1") 537 | .env("AFL_DISABLE_TRIM", "1") 538 | .env("AFL_NO_WARN_INSTABILITY", "1") 539 | .env("AFL_FUZZER_STATS_UPDATE_INTERVAL", "10") 540 | .env("AFL_IMPORT_FIRST", "1") 541 | .env(final_sync, "1") 542 | .env("AFL_IGNORE_SEED_PROBLEMS", "1") 543 | .stdout(log_destination()) 544 | .stderr(log_destination()) 545 | .spawn()?, 546 | ) 547 | } 548 | eprintln!("{} afl ", style(" Launched").green().bold()); 549 | } 550 | 551 | if honggfuzz_jobs > 0 { 552 | let hfuzz_help = process::Command::new(&cargo) 553 | .args(["hfuzz", "run", &self.target]) 554 | .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") 555 | .env("CARGO_TARGET_DIR", "./target/honggfuzz") 556 | .env( 557 | "HFUZZ_WORKSPACE", 558 | format!("{}/honggfuzz", self.output_target()), 559 | ) 560 | .env("HFUZZ_RUN_ARGS", "--help") 561 | .output() 562 | .context("could not run `cargo hfuzz run --help`")?; 563 | 564 | if !std::str::from_utf8(hfuzz_help.stdout.as_slice()) 565 | .unwrap_or_default() 566 | .contains("dynamic_input") 567 | && !std::str::from_utf8(hfuzz_help.stderr.as_slice()) 568 | .unwrap_or_default() 569 | .contains("dynamic_input") 570 | { 571 | return Err(anyhow!("Outdated version of honggfuzz, please update the ziggy version in your Cargo.toml or rebuild the project")); 572 | } 573 | 574 | let dictionary_option = match &self.dictionary { 575 | Some(d) => format!("-w{}", &d.display().to_string()), 576 | None => String::new(), 577 | }; 578 | 579 | let timeout_option = match self.timeout { 580 | Some(t) => format!("-t{t}"), 581 | None => String::new(), 582 | }; 583 | 584 | let memory_option = match &self.memory_limit { 585 | Some(m) => format!("--rlimit_as{}", m), 586 | None => String::new(), 587 | }; 588 | 589 | // The `script` invocation is a trick to get the correct TTY output for honggfuzz 590 | fuzzer_handles.push( 591 | process::Command::new("script") 592 | .args([ 593 | "--flush", 594 | "--quiet", 595 | "-c", 596 | &format!("{} hfuzz run {}", cargo, &self.target), 597 | "/dev/null", 598 | ]) 599 | .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") 600 | .env("CARGO_TARGET_DIR", "./target/honggfuzz") 601 | .env( 602 | "HFUZZ_WORKSPACE", 603 | format!("{}/honggfuzz", self.output_target()), 604 | ) 605 | .env( 606 | "HFUZZ_RUN_ARGS", 607 | format!( 608 | "--input={} -o{}/honggfuzz/corpus -n{honggfuzz_jobs} -F{} --dynamic_input={}/queue {timeout_option} {dictionary_option} {memory_option}", 609 | &self.corpus(), 610 | &self.output_target(), 611 | self.max_length, 612 | self.output_target(), 613 | ), 614 | ) 615 | .stdin(std::process::Stdio::null()) 616 | .stderr(File::create(format!( 617 | "{}/logs/honggfuzz.log", 618 | self.output_target() 619 | ))?) 620 | .stdout(File::create(format!( 621 | "{}/logs/honggfuzz.log", 622 | self.output_target() 623 | ))?) 624 | .spawn()?, 625 | ); 626 | eprintln!( 627 | "{} honggfuzz ", 628 | style(" Launched").green().bold() 629 | ); 630 | } 631 | 632 | eprintln!("\nSee more live information by running:"); 633 | if afl_jobs > 0 { 634 | eprintln!( 635 | " {}", 636 | style(format!("tail -f {}/logs/afl.log", self.output_target())).bold() 637 | ); 638 | } 639 | if afl_jobs > 1 { 640 | eprintln!( 641 | " {}", 642 | style(format!("tail -f {}/logs/afl_1.log", self.output_target())).bold() 643 | ); 644 | } 645 | if honggfuzz_jobs > 0 { 646 | eprintln!( 647 | " {}", 648 | style(format!( 649 | "tail -f {}/logs/honggfuzz.log", 650 | self.output_target() 651 | )) 652 | .bold() 653 | ); 654 | } 655 | 656 | Ok(fuzzer_handles) 657 | } 658 | 659 | fn all_seeds(&self) -> Result> { 660 | Ok(glob(&format!("{}/afl/*/queue/*", self.output_target())) 661 | .map_err(|_| anyhow!("Failed to read AFL++ queue glob pattern"))? 662 | .chain( 663 | glob(&format!("{}/*", self.corpus())) 664 | .map_err(|_| anyhow!("Failed to read Honggfuzz corpus glob pattern"))?, 665 | ) 666 | .flatten() 667 | .filter(|f| f.is_file()) 668 | .collect()) 669 | } 670 | 671 | // Copy all corpora into `corpus` 672 | pub fn copy_corpora(&self) -> Result<()> { 673 | self.all_seeds()?.iter().for_each(|s| { 674 | let _ = fs::copy( 675 | s.to_str().unwrap_or_default(), 676 | format!( 677 | "{}/{}", 678 | &self.corpus_tmp(), 679 | s.file_name() 680 | .unwrap_or_default() 681 | .to_str() 682 | .unwrap_or_default(), 683 | ), 684 | ); 685 | }); 686 | Ok(()) 687 | } 688 | 689 | pub fn run_minimization(&self) -> Result<()> { 690 | let term = Term::stdout(); 691 | 692 | term.write_line(&format!( 693 | "\n {}", 694 | &style("Running minimization").magenta().bold() 695 | ))?; 696 | 697 | let input_corpus = &self.corpus_tmp(); 698 | let minimized_corpus = &self.corpus_minimized(); 699 | 700 | let old_corpus_size = fs::read_dir(input_corpus) 701 | .map_or(String::from("err"), |corpus| format!("{}", corpus.count())); 702 | 703 | let engine = match (self.no_afl, self.no_honggfuzz, self.jobs) { 704 | (false, false, 1) => FuzzingEngines::AFLPlusPlus, 705 | (false, false, _) => FuzzingEngines::All, 706 | (false, true, _) => FuzzingEngines::AFLPlusPlus, 707 | (true, false, _) => FuzzingEngines::Honggfuzz, 708 | (true, true, _) => return Err(anyhow!("Pick at least one fuzzer")), 709 | }; 710 | 711 | let mut minimization_args = Minimize { 712 | target: self.target.clone(), 713 | input_corpus: PathBuf::from(input_corpus), 714 | output_corpus: PathBuf::from(minimized_corpus), 715 | ziggy_output: self.ziggy_output.clone(), 716 | jobs: self.jobs, 717 | engine, 718 | }; 719 | match minimization_args.minimize() { 720 | Ok(_) => { 721 | let new_corpus_size = fs::read_dir(minimized_corpus) 722 | .map_or(String::from("err"), |corpus| format!("{}", corpus.count())); 723 | 724 | term.move_cursor_up(1)?; 725 | 726 | if new_corpus_size == *"err" || new_corpus_size == *"0" { 727 | return Err(anyhow!("Please check the logs and make sure the right version of the fuzzers are installed")); 728 | } else { 729 | term.write_line(&format!( 730 | "{} the corpus ({} -> {} files) \n", 731 | style(" Minimized").magenta().bold(), 732 | old_corpus_size, 733 | new_corpus_size 734 | ))?; 735 | } 736 | } 737 | Err(_) => { 738 | return Err(anyhow!("Please check the logs, this might be an oom error")); 739 | } 740 | }; 741 | Ok(()) 742 | } 743 | 744 | pub fn print_stats(&self, cov_worker_status: &str) { 745 | let fuzzer_name = format!(" {} ", self.target); 746 | 747 | let reset = "\x1b[0m"; 748 | let gray = "\x1b[1;90m"; 749 | let red = "\x1b[1;91m"; 750 | let green = "\x1b[1;92m"; 751 | let yellow = "\x1b[1;93m"; 752 | let purple = "\x1b[1;95m"; 753 | let blue = "\x1b[1;96m"; 754 | 755 | // First step: execute afl-whatsup 756 | let mut afl_status = format!("{green}running{reset} ─"); 757 | let mut afl_total_execs = String::new(); 758 | let mut afl_instances = String::new(); 759 | let mut afl_speed = String::new(); 760 | let mut afl_coverage = String::new(); 761 | let mut afl_crashes = String::new(); 762 | let mut afl_timeouts = String::new(); 763 | let mut afl_new_finds = String::new(); 764 | let mut afl_faves = String::new(); 765 | 766 | if !self.afl() { 767 | afl_status = format!("{yellow}disabled{reset} ") 768 | } else { 769 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 770 | let afl_stats_process = process::Command::new(cargo) 771 | .args([ 772 | "afl", 773 | "whatsup", 774 | "-s", 775 | &format!("{}/afl", self.output_target()), 776 | ]) 777 | .output(); 778 | 779 | if let Ok(process) = afl_stats_process { 780 | let s = std::str::from_utf8(&process.stdout).unwrap_or_default(); 781 | 782 | for mut line in s.split('\n') { 783 | line = line.trim(); 784 | if let Some(total_execs) = line.strip_prefix("Total execs : ") { 785 | afl_total_execs = 786 | String::from(total_execs.split(',').next().unwrap_or_default()); 787 | } else if let Some(instances) = line.strip_prefix("Fuzzers alive : ") { 788 | afl_instances = String::from(instances); 789 | } else if let Some(speed) = line.strip_prefix("Cumulative speed : ") { 790 | afl_speed = String::from(speed); 791 | } else if let Some(coverage) = line.strip_prefix("Coverage reached : ") { 792 | afl_coverage = String::from(coverage); 793 | } else if let Some(crashes) = line.strip_prefix("Crashes saved : ") { 794 | afl_crashes = String::from(crashes); 795 | } else if let Some(timeouts) = line.strip_prefix("Hangs saved : ") { 796 | afl_timeouts = String::from(timeouts.split(' ').next().unwrap_or_default()); 797 | } else if let Some(new_finds) = line.strip_prefix("Time without finds : ") { 798 | afl_new_finds = 799 | String::from(new_finds.split(',').next().unwrap_or_default()); 800 | } else if let Some(pending_items) = line.strip_prefix("Pending items : ") { 801 | afl_faves = String::from( 802 | pending_items 803 | .split(',') 804 | .next() 805 | .unwrap_or_default() 806 | .strip_suffix(" faves") 807 | .unwrap_or_default(), 808 | ); 809 | } 810 | } 811 | } 812 | } 813 | 814 | // Second step: Get stats from honggfuzz logs 815 | let mut hf_status = format!("{green}running{reset} ─"); 816 | let mut hf_total_execs = String::new(); 817 | let mut hf_threads = String::new(); 818 | let mut hf_speed = String::new(); 819 | let mut hf_coverage = String::new(); 820 | let mut hf_crashes = String::new(); 821 | let mut hf_timeouts = String::new(); 822 | let mut hf_new_finds = String::new(); 823 | 824 | if !self.honggfuzz() { 825 | hf_status = format!("{yellow}disabled{reset} "); 826 | } else { 827 | let hf_stats_process = process::Command::new("tail") 828 | .args([ 829 | "-n300", 830 | &format!("{}/logs/honggfuzz.log", self.output_target()), 831 | ]) 832 | .output(); 833 | if let Ok(process) = hf_stats_process { 834 | let s = std::str::from_utf8(&process.stdout).unwrap_or_default(); 835 | for raw_line in s.split('\n') { 836 | let stripped_line = strip_str(raw_line); 837 | let line = stripped_line.trim(); 838 | if let Some(total_execs) = line.strip_prefix("Iterations : ") { 839 | hf_total_execs = 840 | String::from(total_execs.split(' ').next().unwrap_or_default()); 841 | } else if let Some(threads) = line.strip_prefix("Threads : ") { 842 | hf_threads = String::from(threads.split(',').next().unwrap_or_default()); 843 | } else if let Some(speed) = line.strip_prefix("Speed : ") { 844 | hf_speed = String::from( 845 | speed 846 | .split("[avg: ") 847 | .nth(1) 848 | .unwrap_or_default() 849 | .strip_suffix(']') 850 | .unwrap_or_default(), 851 | ) + "/sec"; 852 | } else if let Some(coverage) = line.strip_prefix("Coverage : ") { 853 | hf_coverage = String::from( 854 | coverage 855 | .split('[') 856 | .nth(1) 857 | .unwrap_or_default() 858 | .split(']') 859 | .next() 860 | .unwrap_or_default(), 861 | ); 862 | } else if let Some(crashes) = line.strip_prefix("Crashes : ") { 863 | hf_crashes = String::from(crashes.split(' ').next().unwrap_or_default()); 864 | } else if let Some(timeouts) = line.strip_prefix("Timeouts : ") { 865 | hf_timeouts = String::from(timeouts.split(' ').next().unwrap_or_default()); 866 | } else if let Some(new_finds) = line.strip_prefix("Cov Update : ") { 867 | hf_new_finds = String::from(new_finds.trim()); 868 | hf_new_finds = String::from( 869 | hf_new_finds 870 | .strip_prefix("0 days ") 871 | .unwrap_or(&hf_new_finds), 872 | ); 873 | hf_new_finds = String::from( 874 | hf_new_finds 875 | .strip_prefix("00 hrs ") 876 | .unwrap_or(&hf_new_finds), 877 | ); 878 | hf_new_finds = String::from( 879 | hf_new_finds 880 | .strip_prefix("00 mins ") 881 | .unwrap_or(&hf_new_finds), 882 | ); 883 | hf_new_finds = String::from( 884 | hf_new_finds.strip_suffix(" ago").unwrap_or(&hf_new_finds), 885 | ); 886 | } 887 | } 888 | } 889 | } 890 | 891 | // Third step: Get global stats 892 | let mut total_run_time = time_humanize::HumanTime::from(self.start_time.elapsed()) 893 | .to_text_en( 894 | time_humanize::Accuracy::Rough, 895 | time_humanize::Tense::Present, 896 | ); 897 | if total_run_time == "now" { 898 | total_run_time = String::from("..."); 899 | } 900 | 901 | // Fifth step: Print stats 902 | let mut screen = String::new(); 903 | // We start by clearing the screen 904 | screen += "\x1B[1;1H\x1B[2J"; 905 | screen += &format!("┌─ {blue}ziggy{reset} {purple}rocking{reset} ─────────{fuzzer_name:─^25.25}──────────────────{blue}/{red}////{reset}──┐\n"); 906 | screen += &format!( 907 | "│{gray}run time :{reset} {total_run_time:17.17} {blue}/{red}///{reset} │\n" 908 | ); 909 | screen += &format!("├─ {blue}afl++{reset} {afl_status:0}─────────────────────────────────────────────────────{blue}/{red}///{reset}─┤\n"); 910 | if !afl_status.contains("disabled") { 911 | screen += &format!("│ {gray}instances :{reset} {afl_instances:17.17} │ {gray}best coverage :{reset} {afl_coverage:11.11} {blue}/{red}//{reset} │\n"); 912 | if afl_crashes == "0" { 913 | screen += &format!("│{gray}cumulative speed :{reset} {afl_speed:17.17} │ {gray}crashes saved :{reset} {afl_crashes:11.11} {blue}/{red}/{reset} │\n"); 914 | } else { 915 | screen += &format!("│{gray}cumulative speed :{reset} {afl_speed:17.17} │ {gray}crashes saved :{reset} {red}{afl_crashes:11.11}{reset} {blue}/{red}/{reset} │\n"); 916 | } 917 | screen += &format!( 918 | "│ {gray}total execs :{reset} {afl_total_execs:17.17} │{gray}timeouts saved :{reset} {afl_timeouts:17.17} │\n" 919 | ); 920 | screen += &format!("│ {gray}top inputs todo :{reset} {afl_faves:17.17} │ {gray}no find for :{reset} {afl_new_finds:17.17} │\n"); 921 | } 922 | screen += &format!( 923 | "├─ {blue}honggfuzz{reset} {hf_status:0}─────────────────────────────────────────────────┬────┘\n" 924 | ); 925 | if !hf_status.contains("disabled") { 926 | screen += &format!("│ {gray}threads :{reset} {hf_threads:17.17} │ {gray}coverage :{reset} {hf_coverage:17.17} │\n"); 927 | if hf_crashes == "0" { 928 | screen += &format!("│{gray}average speed :{reset} {hf_speed:17.17} │ {gray}crashes saved :{reset} {hf_crashes:17.17} │\n"); 929 | } else { 930 | screen += &format!("│{gray}average speed :{reset} {hf_speed:17.17} │ {gray}crashes saved :{reset} {red}{hf_crashes:17.17}{reset} │\n"); 931 | } 932 | screen += &format!("│ {gray}total execs :{reset} {hf_total_execs:17.17} │{gray}timeouts saved :{reset} {hf_timeouts:17.17} │\n"); 933 | screen += &format!("│ │ {gray}no find for :{reset} {hf_new_finds:17.17} │\n"); 934 | } 935 | if self.coverage_worker { 936 | screen += &format!( 937 | "├─ {blue}coverage{reset} {green}enabled{reset} ───────────┬───────────────────────────────────────┘\n" 938 | ); 939 | // TODO Add countdown 940 | screen += &format!("│{gray}status :{reset} {cov_worker_status:20.20} │\n"); 941 | screen += "└──────────────────────────────┘"; 942 | } else { 943 | screen += "└──────────────────────────────────────────────────────────────────────┘\n"; 944 | } 945 | eprintln!("{screen}"); 946 | } 947 | } 948 | 949 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 950 | pub enum FuzzingConfig { 951 | Generic, 952 | Binary, 953 | Text, 954 | Blockchain, 955 | } 956 | 957 | impl FuzzingConfig { 958 | fn input_format_flag(&self) -> &str { 959 | match self { 960 | Self::Text => "-atext", 961 | Self::Binary => "-abinary", 962 | _ => "", 963 | } 964 | } 965 | } 966 | 967 | use std::fmt; 968 | 969 | impl fmt::Display for FuzzingConfig { 970 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 971 | write!(f, "{:?}", self) 972 | } 973 | } 974 | 975 | pub fn kill_subprocesses_recursively(pid: &str) -> Result<(), Error> { 976 | let subprocesses = process::Command::new("pgrep") 977 | .arg(format!("-P{pid}")) 978 | .output()?; 979 | 980 | for subprocess in std::str::from_utf8(&subprocesses.stdout)?.split('\n') { 981 | if subprocess.is_empty() { 982 | continue; 983 | } 984 | 985 | kill_subprocesses_recursively(subprocess) 986 | .context("Error in kill_subprocesses_recursively for pid {pid}")?; 987 | } 988 | 989 | unsafe { 990 | libc::kill(pid.parse::().unwrap(), libc::SIGTERM); 991 | } 992 | Ok(()) 993 | } 994 | 995 | // Stop all fuzzer processes 996 | pub fn stop_fuzzers(processes: &mut Vec) -> Result<(), Error> { 997 | for process in processes { 998 | kill_subprocesses_recursively(&process.id().to_string())?; 999 | } 1000 | Ok(()) 1001 | } 1002 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "cli"))] 2 | fn main() {} 3 | 4 | mod add_seeds; 5 | mod build; 6 | mod coverage; 7 | mod fuzz; 8 | mod minimize; 9 | mod plot; 10 | mod run; 11 | mod triage; 12 | 13 | #[cfg(feature = "cli")] 14 | use crate::fuzz::FuzzingConfig; 15 | #[cfg(feature = "cli")] 16 | use anyhow::{anyhow, Context, Result}; 17 | #[cfg(feature = "cli")] 18 | use clap::{Args, Parser, Subcommand, ValueEnum}; 19 | #[cfg(feature = "cli")] 20 | use std::{fs, path::PathBuf}; 21 | 22 | pub const DEFAULT_UNMODIFIED_TARGET: &str = "automatically guessed"; 23 | 24 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 25 | pub enum FuzzingEngines { 26 | All, 27 | AFLPlusPlus, 28 | Honggfuzz, 29 | } 30 | 31 | pub const DEFAULT_OUTPUT_DIR: &str = "./output"; 32 | 33 | pub const DEFAULT_CORPUS_DIR: &str = "{ziggy_output}/{target_name}/corpus/"; 34 | 35 | pub const DEFAULT_COVERAGE_DIR: &str = "{ziggy_output}/{target_name}/coverage/"; 36 | 37 | pub const DEFAULT_MINIMIZATION_DIR: &str = "{ziggy_output}/{target_name}/corpus_minimized/"; 38 | 39 | pub const DEFAULT_PLOT_DIR: &str = "{ziggy_output}/{target_name}/plot/"; 40 | 41 | pub const DEFAULT_CRASHES_DIR: &str = "{ziggy_output}/{target_name}/crashes/"; 42 | 43 | pub const DEFAULT_TRIAGE_DIR: &str = "{ziggy_output}/{target_name}/triage/"; 44 | 45 | #[derive(Parser)] 46 | #[clap(name = "cargo")] 47 | #[clap(bin_name = "cargo")] 48 | pub enum Cargo { 49 | #[clap(subcommand)] 50 | Ziggy(Ziggy), 51 | } 52 | 53 | #[derive(Subcommand)] 54 | #[clap( 55 | author, 56 | version, 57 | about = "A multi-fuzzer management utility for all of your Rust fuzzing needs 🧑‍🎤" 58 | )] 59 | pub enum Ziggy { 60 | /// Build the fuzzer and the runner binaries 61 | Build(Build), 62 | 63 | /// Fuzz targets using different fuzzers in parallel 64 | Fuzz(Fuzz), 65 | 66 | /// Run a specific input or a directory of inputs to analyze backtrace 67 | Run(Run), 68 | 69 | /// Minimize the input corpus using the given fuzzing target 70 | Minimize(Minimize), 71 | 72 | /// Generate code coverage information using the existing corpus 73 | Cover(Cover), 74 | 75 | /// Plot AFL++ data using afl-plot 76 | Plot(Plot), 77 | 78 | /// Add seeds to the running AFL++ fuzzers 79 | AddSeeds(AddSeeds), 80 | 81 | /// Triage crashes found with casr - currently only works for AFL++ 82 | Triage(Triage), 83 | } 84 | 85 | #[derive(Args)] 86 | pub struct Build { 87 | /// No AFL++ (Fuzz only with honggfuzz) 88 | #[clap(long = "no-afl", action)] 89 | no_afl: bool, 90 | 91 | /// No honggfuzz (Fuzz only with AFL++) 92 | #[clap(long = "no-honggfuzz", action)] 93 | no_honggfuzz: bool, 94 | 95 | /// Compile in release mode (--release) 96 | #[clap(long = "release", action)] 97 | release: bool, 98 | 99 | /// Build with ASAN (nightly only) 100 | #[clap(long = "asan", action)] 101 | asan: bool, 102 | } 103 | 104 | #[derive(Args)] 105 | pub struct Fuzz { 106 | /// Target to fuzz 107 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 108 | target: String, 109 | 110 | /// Shared corpus directory 111 | #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_CORPUS_DIR)] 112 | corpus: PathBuf, 113 | 114 | /// Initial corpus directory (will only be read) 115 | #[clap(short, long, value_parser, value_name = "DIR")] 116 | initial_corpus: Option, 117 | 118 | /// Compile in release mode (--release) 119 | #[clap(long = "release", action)] 120 | release: bool, 121 | 122 | /// Fuzzers output directory 123 | #[clap( 124 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 125 | )] 126 | ziggy_output: PathBuf, 127 | 128 | /// Number of concurent fuzzing jobs 129 | #[clap(short, long, value_name = "NUM", default_value_t = 1)] 130 | jobs: u32, 131 | 132 | /// Timeout for a single run 133 | #[clap(short, long, value_name = "SECS")] 134 | timeout: Option, 135 | 136 | /// Memory limit for the fuzz target. (If fuzzing with honggfuzz, a numeric value in MiB mus be specified) 137 | #[clap(short, long, value_name = "STRING")] 138 | memory_limit: Option, 139 | 140 | /// Perform initial minimization 141 | #[clap(short = 'M', long, action, default_value_t = false)] 142 | minimize: bool, 143 | 144 | /// Dictionary file (format:) 145 | #[clap(short = 'x', long = "dict", value_name = "FILE")] 146 | dictionary: Option, 147 | 148 | /// Maximum length of input 149 | #[clap(short = 'G', long = "maxlength", default_value_t = 1048576)] 150 | max_length: u64, 151 | 152 | /// Minimum length of input (AFL++ only) 153 | #[clap(short = 'g', long = "minlength", default_value_t = 1)] 154 | min_length: u64, 155 | 156 | /// No AFL++ (Fuzz only with honggfuzz) 157 | #[clap(long = "no-afl", action)] 158 | no_afl: bool, 159 | 160 | /// No honggfuzz (Fuzz only with AFL++) 161 | #[clap(long = "no-honggfuzz", action)] 162 | no_honggfuzz: bool, 163 | 164 | // This value helps us create a global timer for our display 165 | #[clap(skip = std::time::Instant::now())] 166 | start_time: std::time::Instant, 167 | 168 | /// Pass flags to AFL++ directly 169 | #[clap(short, long)] 170 | afl_flags: Vec, 171 | 172 | /// AFL++ configuration 173 | #[clap(short = 'C', long, default_value = "generic")] 174 | config: FuzzingConfig, 175 | 176 | /// With a coverage worker 177 | #[clap(long)] 178 | coverage_worker: bool, 179 | 180 | /// Coverage generation interval in minutes 181 | #[clap(long, default_value = "15")] 182 | coverage_interval: u64, 183 | 184 | /// Fuzz an already AFL++ instrumented binary; the ziggy way 185 | #[clap(short, long)] 186 | binary: Option, 187 | 188 | /// Build with ASAN (nightly only) 189 | #[clap(long = "asan", action)] 190 | asan: bool, 191 | 192 | /// Foreign fuzzer directories to sync with (AFL++ -F option) 193 | #[clap(long = "foreign-sync", short = 'F', action)] 194 | foreign_sync_dirs: Vec, 195 | } 196 | 197 | #[derive(Args)] 198 | pub struct Run { 199 | /// Target to use 200 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 201 | target: String, 202 | 203 | /// Maximum length of input 204 | #[clap(short = 'G', long = "maxlength", default_value_t = 1048576)] 205 | max_length: u64, 206 | 207 | /// Input directories and/or files to run 208 | #[clap(short, long, value_name = "DIR", default_value = DEFAULT_CORPUS_DIR)] 209 | inputs: Vec, 210 | 211 | /// Recursively run nested directories for all input directories 212 | #[clap(short, long)] 213 | recursive: bool, 214 | 215 | /// Fuzzers output directory 216 | #[clap( 217 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 218 | )] 219 | ziggy_output: PathBuf, 220 | 221 | /// Build with ASAN (nightly only) 222 | #[clap(long = "asan", action)] 223 | asan: bool, 224 | 225 | /// Activate these features on the target 226 | #[clap(short = 'F', long, num_args = 0..)] 227 | features: Vec, 228 | } 229 | 230 | #[derive(Args, Clone)] 231 | pub struct Minimize { 232 | /// Target to use 233 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 234 | target: String, 235 | 236 | /// Corpus directory to minimize 237 | #[clap(short, long, default_value = DEFAULT_CORPUS_DIR)] 238 | input_corpus: PathBuf, 239 | 240 | /// Minimized corpus output directory 241 | #[clap(short, long, default_value = DEFAULT_MINIMIZATION_DIR)] 242 | output_corpus: PathBuf, 243 | 244 | /// Fuzzers output directory 245 | #[clap( 246 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 247 | )] 248 | ziggy_output: PathBuf, 249 | 250 | /// Number of concurent minimizing jobs (AFL++ only) 251 | #[clap(short, long, value_name = "NUM", default_value_t = 1)] 252 | jobs: u32, 253 | 254 | #[clap(short, long, value_enum, default_value_t = FuzzingEngines::All)] 255 | engine: FuzzingEngines, 256 | } 257 | 258 | #[derive(Args)] 259 | pub struct Cover { 260 | /// Target to generate coverage for 261 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 262 | target: String, 263 | 264 | /// Output directory for code coverage report 265 | #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_COVERAGE_DIR)] 266 | output: PathBuf, 267 | 268 | /// Input corpus directory to run target on 269 | #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_CORPUS_DIR)] 270 | input: PathBuf, 271 | 272 | /// Fuzzers output directory 273 | #[clap( 274 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 275 | )] 276 | ziggy_output: PathBuf, 277 | 278 | /// Source directory of covered code 279 | #[clap(short, long, value_parser, value_name = "DIR")] 280 | source: Option, 281 | 282 | /// Keep coverage data files (WARNING: Do not use if source code has changed) 283 | #[clap(short, long, default_value_t = false)] 284 | keep: bool, 285 | 286 | /// Comma separated list of output types. See grov --help to see supported output types. Default: html 287 | #[clap(short = 't', long)] 288 | output_types: Option, 289 | } 290 | 291 | #[derive(Args)] 292 | pub struct Plot { 293 | /// Target to generate plot for 294 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 295 | target: String, 296 | 297 | /// Name of AFL++ fuzzer to use as data source 298 | #[clap(short, long, value_name = "NAME", default_value = "mainaflfuzzer")] 299 | input: String, 300 | 301 | /// Output directory for plot 302 | #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_PLOT_DIR)] 303 | output: PathBuf, 304 | 305 | /// Fuzzers output directory 306 | #[clap( 307 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 308 | )] 309 | ziggy_output: PathBuf, 310 | } 311 | 312 | #[derive(Args)] 313 | pub struct Triage { 314 | /// Target to use 315 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 316 | target: String, 317 | 318 | /// Triage output directory to be written to (will be overwritten) 319 | #[clap(short, long, value_name = "DIR", default_value = DEFAULT_TRIAGE_DIR)] 320 | output: PathBuf, 321 | 322 | /// Number of concurent fuzzing jobs 323 | #[clap(short, long, value_name = "NUM", default_value_t = 1)] 324 | jobs: u32, 325 | 326 | /// Fuzzers output directory 327 | #[clap( 328 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 329 | )] 330 | ziggy_output: PathBuf, 331 | /* future feature, wait for casr 332 | /// Crash directory to be sourced from 333 | #[clap(short, long, value_parser, value_name = "DIR", default_value = DEFAULT_CRASHES_DIR)] 334 | input: PathBuf, 335 | */ 336 | } 337 | 338 | #[derive(Args)] 339 | pub struct AddSeeds { 340 | /// Target to use 341 | #[clap(value_name = "TARGET", default_value = DEFAULT_UNMODIFIED_TARGET)] 342 | target: String, 343 | 344 | /// Seeds directory to be added 345 | #[clap(short, long, value_parser, value_name = "DIR")] 346 | input: PathBuf, 347 | 348 | /// Fuzzers output directory 349 | #[clap( 350 | short, long, env = "ZIGGY_OUTPUT", value_parser, value_name = "DIR", default_value = DEFAULT_OUTPUT_DIR 351 | )] 352 | ziggy_output: PathBuf, 353 | } 354 | 355 | #[cfg(feature = "cli")] 356 | fn main() -> Result<(), anyhow::Error> { 357 | let Cargo::Ziggy(command) = Cargo::parse(); 358 | match command { 359 | Ziggy::Build(args) => args.build().context("Failed to build the fuzzers"), 360 | Ziggy::Fuzz(mut args) => args.fuzz().context("Failure running fuzzers"), 361 | Ziggy::Run(mut args) => args.run().context("Failure running inputs"), 362 | Ziggy::Minimize(mut args) => args.minimize().context("Failure running minimization"), 363 | Ziggy::Cover(mut args) => args 364 | .generate_coverage() 365 | .context("Failure generating coverage"), 366 | Ziggy::Plot(mut args) => args.generate_plot().context("Failure generating plot"), 367 | Ziggy::AddSeeds(mut args) => args.add_seeds().context("Failure addings seeds to AFL"), 368 | Ziggy::Triage(mut args) => args 369 | .triage() 370 | .context("Triaging with casr failed, try \"cargo install casr\""), 371 | } 372 | } 373 | 374 | pub fn find_target(target: &String) -> Result { 375 | // If the target is already set, we're done here 376 | if target != DEFAULT_UNMODIFIED_TARGET { 377 | return Ok(target.into()); 378 | } 379 | 380 | let new_target_result = guess_target(); 381 | 382 | new_target_result.context("Target is not obvious") 383 | } 384 | 385 | fn guess_target() -> Result { 386 | let metadata = cargo_metadata::MetadataCommand::new().exec()?; 387 | let default_package = metadata.workspace_default_members; 388 | if let Some(package_id) = default_package.first() { 389 | if let Some(package) = metadata.packages.iter().find(|p| p.id == *package_id) { 390 | return Ok(package.name.clone()); 391 | } 392 | } 393 | 394 | Err(anyhow!("Please specify a target")) 395 | } 396 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/minimize.rs: -------------------------------------------------------------------------------- 1 | use crate::{find_target, Build, FuzzingEngines, Minimize}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use std::{ 4 | env, 5 | fs::{self, File}, 6 | process, thread, 7 | time::Duration, 8 | }; 9 | use twox_hash::XxHash64; 10 | 11 | impl Minimize { 12 | pub fn minimize(&mut self) -> Result<(), anyhow::Error> { 13 | let build = Build { 14 | no_afl: self.engine == FuzzingEngines::Honggfuzz, 15 | no_honggfuzz: self.engine == FuzzingEngines::AFLPlusPlus, 16 | release: false, 17 | asan: false, 18 | }; 19 | build.build().context("Failed to build the fuzzers")?; 20 | 21 | self.target = 22 | find_target(&self.target).context("⚠️ couldn't find target when minimizing")?; 23 | 24 | if fs::read_dir(self.output_corpus()).is_ok() { 25 | return Err(anyhow!( 26 | "Directory {} exists, please move it before running minimization", 27 | self.output_corpus() 28 | )); 29 | } 30 | 31 | let entries = fs::read_dir(self.input_corpus())?; 32 | let original_count = entries.filter_map(|entry| entry.ok()).count(); 33 | println!("Running minimization on a corpus of {original_count} files"); 34 | 35 | match self.engine { 36 | FuzzingEngines::All => { 37 | let min_afl = self.clone(); 38 | let handle_afl = thread::spawn(move || { 39 | min_afl.minimize_afl().unwrap(); 40 | }); 41 | thread::sleep(Duration::from_millis(1000)); 42 | 43 | let min_honggfuzz = self.clone(); 44 | let handle_honggfuzz = thread::spawn(move || { 45 | min_honggfuzz.minimize_honggfuzz().unwrap(); 46 | }); 47 | 48 | handle_afl.join().unwrap(); 49 | handle_honggfuzz.join().unwrap(); 50 | } 51 | FuzzingEngines::AFLPlusPlus => { 52 | self.minimize_afl()?; 53 | } 54 | FuzzingEngines::Honggfuzz => { 55 | self.minimize_honggfuzz()?; 56 | } 57 | } 58 | 59 | // We rename every file to its md5 hash 60 | let min_entries = fs::read_dir(self.output_corpus())?; 61 | for file in min_entries.flatten() { 62 | let bytes = fs::read(file.path()).unwrap_or_default(); 63 | let hash = XxHash64::oneshot(0, &bytes); 64 | let _ = fs::rename(file.path(), format!("{}/{hash:x}", self.output_corpus())); 65 | } 66 | 67 | let min_entries_hashed = fs::read_dir(self.output_corpus())?; 68 | let minimized_count = min_entries_hashed.filter_map(|entry| entry.ok()).count(); 69 | println!("Minimized corpus contains {minimized_count} files"); 70 | 71 | Ok(()) 72 | } 73 | 74 | fn input_corpus(&self) -> String { 75 | self.input_corpus 76 | .display() 77 | .to_string() 78 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 79 | .replace("{target_name}", &self.target) 80 | } 81 | 82 | fn output_corpus(&self) -> String { 83 | self.output_corpus 84 | .display() 85 | .to_string() 86 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 87 | .replace("{target_name}", &self.target) 88 | } 89 | 90 | // AFL++ minimization 91 | fn minimize_afl(&self) -> Result<(), anyhow::Error> { 92 | println!("Minimizing with AFL++"); 93 | // The cargo executable 94 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 95 | 96 | let jobs_option = match self.jobs { 97 | 0 | 1 => String::from("all"), 98 | t => format!("{t}"), 99 | }; 100 | 101 | // AFL++ minimization 102 | process::Command::new(cargo) 103 | .args([ 104 | "afl", 105 | "cmin", 106 | "-i", 107 | &self.input_corpus(), 108 | "-o", 109 | &self.output_corpus(), 110 | "-T", 111 | &jobs_option, 112 | "--", 113 | &format!("./target/afl/debug/{}", &self.target), 114 | ]) 115 | .stderr(File::create(format!( 116 | "{}/{}/logs/minimization_afl.log", 117 | &self.ziggy_output.display(), 118 | &self.target, 119 | ))?) 120 | .stdout(File::create(format!( 121 | "{}/{}/logs/minimization_afl.log", 122 | &self.ziggy_output.display(), 123 | &self.target, 124 | ))?) 125 | .spawn()? 126 | .wait()?; 127 | Ok(()) 128 | } 129 | 130 | // HONGGFUZZ minimization 131 | fn minimize_honggfuzz(&self) -> Result<(), anyhow::Error> { 132 | println!("Minimizing with honggfuzz"); 133 | // The cargo executable 134 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 135 | 136 | process::Command::new(cargo) 137 | .args(["hfuzz", "run", &self.target]) 138 | .env("CARGO_TARGET_DIR", "./target/honggfuzz") 139 | .env("HFUZZ_BUILD_ARGS", "--features=ziggy/honggfuzz") 140 | .env( 141 | "HFUZZ_WORKSPACE", 142 | format!( 143 | "{}/{}/honggfuzz", 144 | &self.ziggy_output.display(), 145 | &self.target 146 | ), 147 | ) 148 | .env( 149 | "HFUZZ_RUN_ARGS", 150 | format!("-i{} -M -o{}", &self.input_corpus(), &self.output_corpus(),), 151 | ) 152 | .stderr(File::create(format!( 153 | "{}/{}/logs/minimization_honggfuzz.log", 154 | &self.ziggy_output.display(), 155 | &self.target, 156 | ))?) 157 | .stdout(File::create(format!( 158 | "{}/{}/logs/minimization_honggfuzz.log", 159 | &self.ziggy_output.display(), 160 | &self.target, 161 | ))?) 162 | .spawn()? 163 | .wait()?; 164 | Ok(()) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/plot.rs: -------------------------------------------------------------------------------- 1 | use crate::{find_target, Plot}; 2 | use anyhow::{Context, Result}; 3 | use std::{env, process}; 4 | 5 | impl Plot { 6 | pub fn generate_plot(&mut self) -> Result<(), anyhow::Error> { 7 | eprintln!("Generating plot"); 8 | 9 | self.target = 10 | find_target(&self.target).context("⚠️ couldn't find the target for plotting")?; 11 | 12 | // The cargo executable 13 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 14 | 15 | let fuzzer_data_dir = format!( 16 | "{}/{}/afl/{}/", 17 | &self.ziggy_output.display(), 18 | &self.target, 19 | &self.input 20 | ); 21 | 22 | let plot_dir = self 23 | .output 24 | .display() 25 | .to_string() 26 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 27 | .replace("{target_name}", &self.target); 28 | println!("{plot_dir}"); 29 | println!("{}", self.target); 30 | 31 | // We run the afl-plot command 32 | process::Command::new(cargo) 33 | .args(["afl", "plot", &fuzzer_data_dir, &plot_dir]) 34 | .spawn() 35 | .context("⚠️ couldn't spawn afl plot")? 36 | .wait() 37 | .context("⚠️ couldn't wait for the afl plot")?; 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/run.rs: -------------------------------------------------------------------------------- 1 | use crate::{build::ASAN_TARGET, find_target, Run}; 2 | use anyhow::{anyhow, Context, Result}; 3 | use console::style; 4 | use std::{ 5 | collections::HashSet, 6 | env, fs, 7 | os::unix::process::ExitStatusExt, 8 | path::{Path, PathBuf}, 9 | process, 10 | }; 11 | 12 | impl Run { 13 | // Run inputs 14 | pub fn run(&mut self) -> Result<(), anyhow::Error> { 15 | let cargo = env::var("CARGO").unwrap_or_else(|_| String::from("cargo")); 16 | let target = find_target(&self.target)?; 17 | 18 | let mut args = vec!["rustc", "--target-dir=target/runner"]; 19 | let asan_target_str = format!("--target={ASAN_TARGET}"); 20 | let mut rust_flags = env::var("RUSTFLAGS").unwrap_or_default(); 21 | let mut rust_doc_flags = env::var("RUSTDOCFLAGS").unwrap_or_default(); 22 | 23 | for feature in &self.features { 24 | args.extend(["-F", feature.as_str()]); 25 | } 26 | 27 | if self.asan { 28 | args.push(&asan_target_str); 29 | args.extend(["-Z", "build-std"]); 30 | rust_flags.push_str(" -Zsanitizer=address "); 31 | rust_flags.push_str(" -Copt-level=0 "); 32 | rust_doc_flags.push_str(" -Zsanitizer=address "); 33 | }; 34 | 35 | // We build the runner 36 | eprintln!(" {} runner", style("Building").red().bold()); 37 | 38 | // We run the compilation command 39 | let run = process::Command::new(cargo) 40 | .args(args) 41 | .env("RUSTFLAGS", rust_flags) 42 | .env("RUSTDOCFLAGS", rust_doc_flags) 43 | .spawn() 44 | .context("⚠️ couldn't spawn runner compilation")? 45 | .wait() 46 | .context("⚠️ couldn't wait for the runner compilation process")?; 47 | 48 | if !run.success() { 49 | return Err(anyhow!( 50 | "Error building runner: Exited with {:?}", 51 | run.code() 52 | )); 53 | } 54 | 55 | eprintln!(" {} runner", style("Finished").cyan().bold()); 56 | 57 | if self.recursive { 58 | let mut all_dirs = HashSet::new(); 59 | for input in &self.inputs { 60 | all_dirs.insert(input.clone()); 61 | collect_dirs_recursively(input, &mut all_dirs)?; 62 | } 63 | for dir in all_dirs { 64 | if !self.inputs.contains(&dir) { 65 | self.inputs.push(dir); 66 | } 67 | } 68 | } 69 | 70 | let run_args: Vec = self 71 | .inputs 72 | .iter() 73 | .map(|x| { 74 | x.display() 75 | .to_string() 76 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 77 | .replace("{target_name}", &target) 78 | }) 79 | .collect(); 80 | 81 | let runner_path = match self.asan { 82 | true => format!("./target/runner/{ASAN_TARGET}/debug/{}", target), 83 | false => format!("./target/runner/debug/{}", target), 84 | }; 85 | 86 | let res = process::Command::new(runner_path) 87 | .args(run_args) 88 | .env("RUST_BACKTRACE", "full") 89 | .spawn() 90 | .context("⚠️ couldn't spawn the runner process")? 91 | .wait() 92 | .context("⚠️ couldn't wait for the runner process")?; 93 | 94 | if !res.success() { 95 | if let Some(signal) = res.signal() { 96 | println!("⚠️ input terminated with signal {:?}!", signal); 97 | } else if let Some(exit_code) = res.code() { 98 | println!("⚠️ input terminated with code {:?}!", exit_code); 99 | } else { 100 | println!("⚠️ input terminated but we do not know why!"); 101 | } 102 | } 103 | 104 | Ok(()) 105 | } 106 | } 107 | 108 | fn collect_dirs_recursively( 109 | dir: &Path, 110 | dir_list: &mut HashSet, 111 | ) -> Result<(), anyhow::Error> { 112 | if dir.is_dir() { 113 | for entry in fs::read_dir(dir)? { 114 | let entry = entry?; 115 | let path = entry.path(); 116 | if path.is_dir() && !dir_list.contains(&path) { 117 | dir_list.insert(path.clone()); 118 | collect_dirs_recursively(&path, dir_list)?; 119 | } 120 | } 121 | } 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /src/bin/cargo-ziggy/triage.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::process; 3 | 4 | impl Triage { 5 | pub fn triage(&mut self) -> Result<(), anyhow::Error> { 6 | eprintln!("Running CASR triage on crashes"); 7 | 8 | self.target = find_target(&self.target)?; 9 | let input_dir = format!("{}/{}/afl", self.ziggy_output.display(), self.target); 10 | 11 | let triage_dir = self 12 | .output 13 | .display() 14 | .to_string() 15 | .replace("{ziggy_output}", &self.ziggy_output.display().to_string()) 16 | .replace("{target_name}", &self.target); 17 | fs::remove_dir_all(&triage_dir).unwrap_or_default(); 18 | 19 | if !fs::metadata(&input_dir) 20 | .map(|meta| meta.is_dir()) 21 | .unwrap_or(false) 22 | { 23 | eprintln!("This option requires that at least one AFL++ instance was run!"); 24 | return Ok(()); 25 | } 26 | 27 | if fs::metadata(&triage_dir) 28 | .map(|meta| meta.is_dir()) 29 | .unwrap_or(false) 30 | { 31 | eprintln!("Please remove {:?} first", triage_dir); 32 | return Ok(()); 33 | } 34 | 35 | let tool = String::from("casr-afl"); 36 | process::Command::new(tool.clone()) 37 | .args( 38 | [ 39 | "-i", 40 | &input_dir, 41 | "-o", 42 | &triage_dir, 43 | &format!("-j{}", self.jobs), 44 | // future: add option for crashes directory and use runner 45 | ] 46 | .iter() 47 | .filter(|a| a != &&""), 48 | ) 49 | .spawn()? 50 | .wait()?; 51 | 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #[cfg(feature = "afl")] 3 | pub use afl::fuzz as afl_fuzz; 4 | #[cfg(feature = "coverage")] 5 | pub use fork; 6 | #[cfg(feature = "honggfuzz")] 7 | pub use honggfuzz::fuzz as honggfuzz_fuzz; 8 | 9 | // This is our inner harness handler function for the runner. 10 | // We open the input file and feed the data to the harness closure. 11 | #[doc(hidden)] 12 | #[cfg(not(feature = "coverage"))] 13 | pub fn read_file_and_fuzz(mut closure: F, file: String) 14 | where 15 | F: FnMut(&[u8]), 16 | { 17 | use std::{fs::File, io::Read}; 18 | println!("Now running file {file}"); 19 | let mut buffer: Vec = Vec::new(); 20 | match File::open(file) { 21 | Ok(mut f) => { 22 | match f.read_to_end(&mut buffer) { 23 | Ok(_) => { 24 | closure(buffer.as_slice()); 25 | } 26 | Err(e) => { 27 | println!("Could not get data from file: {e}"); 28 | } 29 | }; 30 | } 31 | Err(e) => { 32 | println!("Error opening file: {e}"); 33 | } 34 | }; 35 | } 36 | 37 | // This is our special coverage harness runner. 38 | // We open the input file and feed the data to the harness closure. 39 | // The difference with the runner is that we catch any kind of panic. 40 | #[cfg(feature = "coverage")] 41 | pub fn read_file_and_fuzz(mut closure: F, file: String) 42 | where 43 | F: FnMut(&[u8]), 44 | { 45 | use std::{fs::File, io::Read, process::exit}; 46 | println!("Now running file {file} for coverage"); 47 | let mut buffer: Vec = Vec::new(); 48 | match File::open(file) { 49 | Ok(mut f) => { 50 | match f.read_to_end(&mut buffer) { 51 | Ok(_) => { 52 | use crate::fork::{fork, Fork}; 53 | 54 | match fork() { 55 | Ok(Fork::Parent(child)) => { 56 | println!( 57 | "Continuing execution in parent process, new child has pid: {}", 58 | child 59 | ); 60 | unsafe { 61 | let mut status = 0i32; 62 | let _ = libc::waitpid(child, &mut status, 0); 63 | } 64 | println!("Child is done, moving on"); 65 | } 66 | Ok(Fork::Child) => { 67 | closure(buffer.as_slice()); 68 | exit(0); 69 | } 70 | Err(_) => println!("Fork failed"), 71 | } 72 | } 73 | Err(e) => { 74 | println!("Could not get data from file: {e}"); 75 | } 76 | }; 77 | } 78 | Err(e) => { 79 | println!("Error opening file: {e}"); 80 | } 81 | }; 82 | } 83 | 84 | // This is our middle harness handler macro for the runner and for coverage. 85 | // We read input files and directories from the command line and run the inner harness `fuzz`. 86 | #[doc(hidden)] 87 | #[macro_export] 88 | macro_rules! read_args_and_fuzz { 89 | ( |$buf:ident| $body:block ) => { 90 | use std::{env, fs}; 91 | let args: Vec = env::args().collect(); 92 | for path in &args[1..] { 93 | if let Ok(metadata) = fs::metadata(&path) { 94 | let files = match metadata.is_dir() { 95 | true => fs::read_dir(&path) 96 | .unwrap() 97 | .map(|x| x.unwrap().path()) 98 | .filter(|x| x.is_file()) 99 | .map(|x| x.to_str().unwrap().to_string()) 100 | .collect::>(), 101 | false => vec![path.to_string()], 102 | }; 103 | 104 | for file in files { 105 | $crate::read_file_and_fuzz(|$buf| $body, file); 106 | } 107 | println!("Finished reading all files"); 108 | } else { 109 | println!("Could not read metadata for {path}"); 110 | } 111 | } 112 | }; 113 | } 114 | 115 | /// Fuzz a closure-like block of code by passing an object of arbitrary type. 116 | /// 117 | /// It can handle different types of arguments for the harness closure, including Arbitrary. 118 | /// 119 | /// See [our examples](https://github.com/srlabs/ziggy/tree/main/examples). 120 | /// 121 | /// ```no_run 122 | /// # fn main() { 123 | /// ziggy::fuzz!(|data: &[u8]| { 124 | /// if data.len() != 6 {return} 125 | /// if data[0] != b'q' {return} 126 | /// if data[1] != b'w' {return} 127 | /// if data[2] != b'e' {return} 128 | /// if data[3] != b'r' {return} 129 | /// if data[4] != b't' {return} 130 | /// if data[5] != b'y' {return} 131 | /// panic!("BOOM") 132 | /// }); 133 | /// # } 134 | /// ``` 135 | #[macro_export] 136 | macro_rules! inner_fuzz { 137 | (|$buf:ident| $body:block) => { 138 | $crate::read_args_and_fuzz!(|$buf| $body); 139 | }; 140 | (|$buf:ident: &[u8]| $body:block) => { 141 | $crate::read_args_and_fuzz!(|$buf| $body); 142 | }; 143 | (|$buf:ident: $dty: ty| $body:block) => { 144 | $crate::read_args_and_fuzz!(|$buf| { 145 | let $buf: $dty = { 146 | let mut data = ::arbitrary::Unstructured::new($buf); 147 | if let Ok(d) = ::arbitrary::Arbitrary::arbitrary(&mut data).map_err(|_| "") { 148 | d 149 | } else { 150 | return; 151 | } 152 | }; 153 | $body 154 | }); 155 | }; 156 | } 157 | 158 | /// We need this wrapper 159 | #[macro_export] 160 | #[cfg(not(any(feature = "afl", feature = "honggfuzz")))] 161 | macro_rules! fuzz { 162 | ( $($x:tt)* ) => { 163 | $crate::inner_fuzz!($($x)*); 164 | } 165 | } 166 | 167 | #[macro_export] 168 | #[cfg(feature = "afl")] 169 | macro_rules! fuzz { 170 | ( $($x:tt)* ) => { 171 | static USE_ARGS: std::sync::LazyLock = std::sync::LazyLock::new(|| std::env::args().len() > 1); 172 | if *USE_ARGS { 173 | $crate::inner_fuzz!($($x)*); 174 | } else { 175 | $crate::afl_fuzz!($($x)*); 176 | } 177 | }; 178 | } 179 | 180 | #[macro_export] 181 | #[cfg(feature = "honggfuzz")] 182 | macro_rules! fuzz { 183 | ( $($x:tt)* ) => { 184 | loop { 185 | $crate::honggfuzz_fuzz!($($x)*); 186 | } 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /tests/arbitrary_fuzz.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::PathBuf, 4 | process, thread, 5 | time::{Duration, SystemTime, UNIX_EPOCH}, 6 | }; 7 | 8 | fn kill_subprocesses_recursively(pid: &str) { 9 | let subprocesses = process::Command::new("pgrep") 10 | .arg(format!("-P{pid}")) 11 | .output() 12 | .unwrap(); 13 | 14 | for subprocess in std::str::from_utf8(&subprocesses.stdout) 15 | .unwrap() 16 | .split('\n') 17 | { 18 | if subprocess.is_empty() { 19 | continue; 20 | } 21 | 22 | kill_subprocesses_recursively(subprocess); 23 | } 24 | 25 | println!("Killing pid {pid}"); 26 | unsafe { 27 | libc::kill(pid.parse::().unwrap(), libc::SIGTERM); 28 | } 29 | } 30 | 31 | #[allow(clippy::zombie_processes)] 32 | #[test] 33 | fn integration() { 34 | let unix_time = format!( 35 | "{}", 36 | SystemTime::now() 37 | .duration_since(UNIX_EPOCH) 38 | .unwrap() 39 | .as_secs() 40 | ); 41 | let temp_dir_path = env::temp_dir().join(unix_time); 42 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 43 | let workspace_root: PathBuf = metadata.workspace_root.into(); 44 | let target_directory: PathBuf = metadata.target_directory.into(); 45 | let cargo_ziggy = target_directory.join("debug").join("cargo-ziggy"); 46 | let fuzzer_directory = workspace_root.join("examples").join("arbitrary"); 47 | 48 | // TODO Custom target path 49 | 50 | // cargo ziggy build 51 | let build_status = process::Command::new(cargo_ziggy.clone()) 52 | .arg("ziggy") 53 | .arg("build") 54 | .current_dir(fuzzer_directory.clone()) 55 | .status() 56 | .expect("failed to run `cargo ziggy build`"); 57 | 58 | assert!(build_status.success(), "`cargo ziggy build` failed"); 59 | 60 | // cargo ziggy fuzz -j 2 -t 5 -o temp_dir 61 | let fuzzer = process::Command::new(cargo_ziggy) 62 | .arg("ziggy") 63 | .arg("fuzz") 64 | .arg("-j2") 65 | .arg("-t5") 66 | .arg("-G100") 67 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 68 | .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") 69 | .env("AFL_SKIP_CPUFREQ", "1") 70 | .current_dir(fuzzer_directory) 71 | .spawn() 72 | .expect("failed to run `cargo ziggy fuzz`"); 73 | thread::sleep(Duration::from_secs(10)); 74 | kill_subprocesses_recursively(&format!("{}", fuzzer.id())); 75 | 76 | assert!(temp_dir_path 77 | .join("arbitrary-fuzz") 78 | .join("afl") 79 | .join("mainaflfuzzer") 80 | .join("fuzzer_stats") 81 | .is_file()); 82 | assert!( 83 | fs::read_dir( 84 | temp_dir_path 85 | .join("arbitrary-fuzz") 86 | .join("afl") 87 | .join("mainaflfuzzer") 88 | .join("crashes") 89 | ) 90 | .unwrap() 91 | .count() 92 | != 0 93 | ); 94 | assert!(temp_dir_path 95 | .join("arbitrary-fuzz") 96 | .join("honggfuzz") 97 | .join("arbitrary-fuzz") 98 | .join("input") 99 | .is_dir()); 100 | } 101 | -------------------------------------------------------------------------------- /tests/asan_fuzz.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::PathBuf, 4 | process, thread, 5 | time::{Duration, SystemTime, UNIX_EPOCH}, 6 | }; 7 | 8 | fn kill_subprocesses_recursively(pid: &str) { 9 | let subprocesses = process::Command::new("pgrep") 10 | .arg(format!("-P{pid}")) 11 | .output() 12 | .unwrap(); 13 | 14 | for subprocess in std::str::from_utf8(&subprocesses.stdout) 15 | .unwrap() 16 | .split('\n') 17 | { 18 | if subprocess.is_empty() { 19 | continue; 20 | } 21 | 22 | kill_subprocesses_recursively(subprocess); 23 | } 24 | 25 | println!("Killing pid {pid}"); 26 | unsafe { 27 | libc::kill(pid.parse::().unwrap(), libc::SIGTERM); 28 | } 29 | } 30 | 31 | #[allow(clippy::zombie_processes)] 32 | #[test] 33 | fn asan_crashes() { 34 | // Not optimal but seems to work fine 35 | if !env!("CARGO").contains("nightly") { 36 | println!("Not running nightly, skipping"); 37 | return; 38 | } 39 | let unix_time = format!( 40 | "{}", 41 | SystemTime::now() 42 | .duration_since(UNIX_EPOCH) 43 | .unwrap() 44 | .as_secs() 45 | ); 46 | let temp_dir_path = env::temp_dir().join(unix_time); 47 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 48 | let workspace_root: PathBuf = metadata.workspace_root.into(); 49 | let target_directory: PathBuf = metadata.target_directory.into(); 50 | let cargo_ziggy = target_directory.join("debug").join("cargo-ziggy"); 51 | let fuzzer_directory = workspace_root.join("examples").join("asan"); 52 | 53 | // TODO Custom target path 54 | 55 | // cargo ziggy build 56 | let build_status = process::Command::new(&cargo_ziggy) 57 | .arg("ziggy") 58 | .arg("build") 59 | .arg("--asan") 60 | .arg("--no-honggfuzz") 61 | .current_dir(&fuzzer_directory) 62 | .status() 63 | .expect("failed to run `cargo ziggy build`"); 64 | 65 | assert!(build_status.success(), "`cargo ziggy build` failed"); 66 | 67 | // cargo ziggy fuzz --asan 68 | let fuzzer = process::Command::new(&cargo_ziggy) 69 | .arg("ziggy") 70 | .arg("fuzz") 71 | .arg("--asan") 72 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 73 | .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") 74 | .env("AFL_SKIP_CPUFREQ", "1") 75 | .current_dir(&fuzzer_directory) 76 | .spawn() 77 | .expect("failed to run `cargo ziggy fuzz`"); 78 | thread::sleep(Duration::from_secs(40)); 79 | kill_subprocesses_recursively(&format!("{}", fuzzer.id())); 80 | 81 | assert!(temp_dir_path 82 | .join("asan-fuzz") 83 | .join("afl") 84 | .join("mainaflfuzzer") 85 | .join("fuzzer_stats") 86 | .is_file()); 87 | assert!( 88 | fs::read_dir( 89 | temp_dir_path 90 | .join("asan-fuzz") 91 | .join("afl") 92 | .join("mainaflfuzzer") 93 | .join("crashes") 94 | ) 95 | .unwrap() 96 | .count() 97 | != 0 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /tests/url_fuzz.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | path::PathBuf, 4 | process, thread, 5 | time::{Duration, SystemTime, UNIX_EPOCH}, 6 | }; 7 | 8 | fn kill_subprocesses_recursively(pid: &str) { 9 | let subprocesses = process::Command::new("pgrep") 10 | .arg(format!("-P{pid}")) 11 | .output() 12 | .unwrap(); 13 | 14 | for subprocess in std::str::from_utf8(&subprocesses.stdout) 15 | .unwrap() 16 | .split('\n') 17 | { 18 | if subprocess.is_empty() { 19 | continue; 20 | } 21 | 22 | kill_subprocesses_recursively(subprocess); 23 | } 24 | 25 | println!("Killing pid {pid}"); 26 | unsafe { 27 | libc::kill(pid.parse::().unwrap(), libc::SIGTERM); 28 | } 29 | } 30 | 31 | #[allow(clippy::zombie_processes)] 32 | #[test] 33 | fn integration() { 34 | let unix_time = format!( 35 | "{}", 36 | SystemTime::now() 37 | .duration_since(UNIX_EPOCH) 38 | .unwrap() 39 | .as_secs() 40 | ); 41 | let temp_dir_path = env::temp_dir().join(unix_time); 42 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 43 | let workspace_root: PathBuf = metadata.workspace_root.into(); 44 | let target_directory: PathBuf = metadata.target_directory.into(); 45 | let cargo_ziggy = target_directory.join("debug").join("cargo-ziggy"); 46 | let fuzzer_directory = workspace_root.join("examples").join("url"); 47 | 48 | // TODO Custom target path 49 | 50 | // cargo ziggy build 51 | let build_status = process::Command::new(&cargo_ziggy) 52 | .arg("ziggy") 53 | .arg("build") 54 | .current_dir(&fuzzer_directory) 55 | .status() 56 | .expect("failed to run `cargo ziggy build`"); 57 | 58 | assert!(build_status.success(), "`cargo ziggy build` failed"); 59 | 60 | // cargo ziggy fuzz -j 2 -t 5 61 | let fuzzer = process::Command::new(&cargo_ziggy) 62 | .arg("ziggy") 63 | .arg("fuzz") 64 | .arg("-j2") 65 | .arg("-t5") 66 | .arg("-G100") 67 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 68 | .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") 69 | .env("AFL_SKIP_CPUFREQ", "1") 70 | .current_dir(&fuzzer_directory) 71 | .spawn() 72 | .expect("failed to run `cargo ziggy fuzz`"); 73 | thread::sleep(Duration::from_secs(10)); 74 | kill_subprocesses_recursively(&format!("{}", fuzzer.id())); 75 | 76 | assert!(temp_dir_path 77 | .join("url-fuzz") 78 | .join("afl") 79 | .join("mainaflfuzzer") 80 | .join("fuzzer_stats") 81 | .is_file()); 82 | assert!(temp_dir_path 83 | .join("url-fuzz") 84 | .join("honggfuzz") 85 | .join("url-fuzz") 86 | .join("input") 87 | .is_dir()); 88 | 89 | // We resume fuzzing 90 | // cargo ziggy fuzz -j 2 -t 5 91 | let fuzzer = process::Command::new(&cargo_ziggy) 92 | .arg("ziggy") 93 | .arg("fuzz") 94 | .arg("-j2") 95 | .arg("-t5") 96 | .arg("-G100") 97 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 98 | .env("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES", "1") 99 | .env("AFL_SKIP_CPUFREQ", "1") 100 | .current_dir(&fuzzer_directory) 101 | .spawn() 102 | .expect("failed to run `cargo ziggy fuzz`"); 103 | thread::sleep(Duration::from_secs(10)); 104 | kill_subprocesses_recursively(&format!("{}", fuzzer.id())); 105 | 106 | // cargo ziggy minimize 107 | let minimization = process::Command::new(&cargo_ziggy) 108 | .arg("ziggy") 109 | .arg("minimize") 110 | .arg("-j2") 111 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 112 | .current_dir(&fuzzer_directory) 113 | .status() 114 | .expect("failed to run `cargo ziggy minimize`"); 115 | 116 | assert!(minimization.success()); 117 | assert!(temp_dir_path 118 | .join("url-fuzz") 119 | .join("logs") 120 | .join("minimization_afl.log") 121 | .is_file()); 122 | 123 | fs::remove_dir_all(temp_dir_path.join("url-fuzz").join("corpus_minimized")).unwrap(); 124 | 125 | // cargo ziggy minimize -e honggfuzz 126 | let minimization = process::Command::new(&cargo_ziggy) 127 | .arg("ziggy") 128 | .arg("minimize") 129 | .arg("-ehonggfuzz") 130 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 131 | .current_dir(&fuzzer_directory) 132 | .status() 133 | .expect("failed to run `cargo ziggy minimize`"); 134 | 135 | assert!(minimization.success()); 136 | assert!(temp_dir_path 137 | .join("url-fuzz") 138 | .join("logs") 139 | .join("minimization_honggfuzz.log") 140 | .is_file()); 141 | 142 | // cargo ziggy cover 143 | let coverage = process::Command::new(&cargo_ziggy) 144 | .arg("ziggy") 145 | .arg("cover") 146 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 147 | .current_dir(&fuzzer_directory) 148 | .status() 149 | .expect("failed to run `cargo ziggy cover`"); 150 | 151 | assert!(coverage.success()); 152 | assert!(temp_dir_path 153 | .join("url-fuzz") 154 | .join("coverage") 155 | .join("index.html") 156 | .is_file()); 157 | 158 | // cargo ziggy plot 159 | let plot = process::Command::new(&cargo_ziggy) 160 | .arg("ziggy") 161 | .arg("plot") 162 | .env("ZIGGY_OUTPUT", format!("{}", temp_dir_path.display())) 163 | .current_dir(&fuzzer_directory) 164 | .status() 165 | .expect("failed to run `cargo ziggy plot`"); 166 | 167 | assert!(plot.success()); 168 | assert!(temp_dir_path 169 | .join("url-fuzz") 170 | .join("plot") 171 | .join("index.html") 172 | .is_file()); 173 | } 174 | --------------------------------------------------------------------------------