diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a1a784468..8dfc045e7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,7 +34,13 @@ jobs: matrix: environment: [ubuntu-latest, macos-latest] toolchain: [stable, nightly] + features: [default, plugins] cc: [cc, clang] + exclude: + - environment: macos-latest + features: plugins + - toolchain: stable + features: plugins include: - cc: cc cxx: c++ @@ -45,21 +51,37 @@ jobs: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} steps: - - if: ${{ matrix.environment == 'ubuntu-latest' }} - run: sudo apt-get install llvm - uses: actions/checkout@v4 with: submodules: true - name: Rustup run: rustup default ${{ matrix.toolchain }} + - name: Install LLVM + id: install-llvm + run: | + LLVM_VERSION="$(rustc --version -v | grep '^LLVM version:' | grep -o '[0-9]\+' | head -n 1)" + if [[ ${{ matrix.environment }} = 'macos-latest' ]]; then + brew update + brew install llvm@"$LLVM_VERSION" || true + echo "PATH=/usr/local/opt/llvm/bin:$PATH" >> "$GITHUB_OUTPUT" + else + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh "$LLVM_VERSION" + echo "PATH=$PATH" >> "$GITHUB_OUTPUT" + fi + env: + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - name: Build - run: cargo build -vv + run: cargo build --features=${{ matrix.features }} -vv + env: + PATH: ${{ steps.install-llvm.outputs.path }} - name: Run afl-system-config - run: cargo run -- afl system-config + run: cargo run --features=${{ matrix.features }} -- afl system-config - name: Build examples (with AFL instrumentation) - run: cargo run -- afl build --examples -vv + run: cargo run --features=${{ matrix.features }} -- afl build --examples -vv - name: Run tests - run: cargo test -p cargo-afl -vv + run: cargo test --features=${{ matrix.features }} -p cargo-afl -vv all-checks: needs: [lint, build] runs-on: ubuntu-latest diff --git a/afl/examples/cmplog.rs b/afl/examples/cmplog.rs index 285d63390..55e0ae6dc 100644 --- a/afl/examples/cmplog.rs +++ b/afl/examples/cmplog.rs @@ -2,7 +2,7 @@ fn main() { // This fuzz harness demonstrates the capabilities of CmpLog. // Simply run the fuzzer and it should find the crash immediately. afl::fuzz!(|data: &[u8]| { - if data.len() != 16 { + if data.len() < 29 { return; } if data[0] != b'A' { @@ -26,6 +26,13 @@ fn main() { return; }; + let slice = &data[16..]; + let match_string = "Hello, world!"; + let compare_string = String::from_utf8(slice.to_vec()).unwrap_or_default(); + if compare_string != match_string { + return; + } + panic!("BOOM"); }); } diff --git a/cargo-afl/Cargo.toml b/cargo-afl/Cargo.toml index 0ef11e18e..b1478c62c 100644 --- a/cargo-afl/Cargo.toml +++ b/cargo-afl/Cargo.toml @@ -32,3 +32,7 @@ assert_cmd = "2.0" ctor = "0.2" predicates = "3.0" tempfile = "3.8" + +[features] +default = [] +plugins = [] diff --git a/cargo-afl/build.rs b/cargo-afl/build.rs index 425072b85..1f829ae91 100644 --- a/cargo-afl/build.rs +++ b/cargo-afl/build.rs @@ -60,21 +60,34 @@ fn main() { build_afl(&work_dir, base.as_deref()); build_afl_llvm_runtime(&work_dir, base.as_deref()); + + if cfg!(feature = "plugins") { + copy_afl_llvm_plugins(&work_dir, base.as_deref()); + } } fn build_afl(work_dir: &Path, base: Option<&Path>) { + // if you had already installed cargo-afl previously you **must** clean AFL++ let mut command = Command::new("make"); command .current_dir(work_dir) - .args(["clean", "all", "install"]) + .args(["clean", "install"]) // skip the checks for the legacy x86 afl-gcc compiler .env("AFL_NO_X86", "1") // build just the runtime to avoid troubles with Xcode clang on macOS - .env("NO_BUILD", "1") + //.env("NO_BUILD", "1") .env("DESTDIR", common::afl_dir(base)) .env("PREFIX", "") .env_remove("DEBUG"); - let status = command.status().expect("could not run 'make'"); + + if cfg!(feature = "plugins") { + let llvm_config = check_llvm_and_get_config(); + command.env("LLVM_CONFIG", llvm_config); + } + + let status = command + .status() + .expect("could not run 'make clean install'"); assert!(status.success()); } @@ -94,6 +107,68 @@ fn build_afl_llvm_runtime(work_dir: &Path, base: Option<&Path>) { assert!(status.success()); } +fn copy_afl_llvm_plugins(work_dir: &Path, base: Option<&Path>) { + // Iterate over the files in the directory. + if let Ok(entries) = work_dir.read_dir() { + for entry in entries.flatten() { + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy(); + + // Get the file extension. + if let Some(extension) = file_name_str.split('.').last() { + // Only copy the files that are shared objects + if extension == "so" { + // Attempt to copy the shared object file. + std::fs::copy( + work_dir.join(&file_name), + common::afl_llvm_dir(base).join(&file_name), + ) + .unwrap_or_else(|_| { + panic!("Couldn't copy shared object file {file_name_str}",) + }); + } + } + } + } else { + eprintln!("Failed to read the work directory. Aborting."); + } +} + +fn check_llvm_and_get_config() -> String { + // Make sure we are on nightly for the -Z flags + assert!( + rustc_version::version_meta().unwrap().channel == rustc_version::Channel::Nightly, + "cargo-afl must be compiled with nightly for the plugins feature" + ); + let version_meta = rustc_version::version_meta().unwrap(); + let llvm_version = version_meta.llvm_version.unwrap().major.to_string(); + + // Fetch the llvm version of the rust toolchain and set the LLVM_CONFIG environment variable to the same version + // This is needed to compile the llvm plugins (needed for cmplog) from afl with the right LLVM version + let llvm_config = if cfg!(target_os = "macos") { + "llvm-config".to_string() + } else { + format!("llvm-config-{llvm_version}") + }; + + // check if llvm tools are installed and with the good version for the plugin compilation + let mut command = Command::new(llvm_config.clone()); + command.args(["--version"]); + let out = command + .output() + .unwrap_or_else(|_| panic!("could not run {llvm_config} --version")); + + let version = String::from_utf8(out.stdout) + .expect("could not convert llvm-config --version output to utf8"); + let major = version + .split('.') + .next() + .expect("could not get major from llvm-config --version output"); + assert!(major == llvm_version); + + llvm_config +} + #[cfg(unix)] mod sys { use std::fs::File; diff --git a/cargo-afl/src/bin/cargo-afl.rs b/cargo-afl/src/bin/cargo-afl.rs index 4823416c9..883490da6 100644 --- a/cargo-afl/src/bin/cargo-afl.rs +++ b/cargo-afl/src/bin/cargo-afl.rs @@ -1,5 +1,4 @@ -use clap::crate_version; - +use std::collections::HashMap; use std::env; use std::ffi::{OsStr, OsString}; use std::process::{self, Command, Stdio}; @@ -99,22 +98,28 @@ fn main() { fn clap_app() -> clap::Command { use clap::{value_parser, Arg, Command}; - let help = "In addition to the subcommands above, Cargo subcommands are also \ + const HELP: &str = "In addition to the subcommands above, Cargo subcommands are also \ supported (see `cargo help` for a list of all Cargo subcommands)."; + const VERSION: &str = if cfg!(feature = "plugins") { + concat!(env!("CARGO_PKG_VERSION"), " [feature=plugins]") + } else { + env!("CARGO_PKG_VERSION") + }; + Command::new("cargo afl") .display_name("cargo") .subcommand_required(true) .arg_required_else_help(true) .subcommand( Command::new("afl") - .version(crate_version!()) + .version(VERSION) .subcommand_required(true) .arg_required_else_help(true) .allow_external_subcommands(true) .external_subcommand_value_parser(value_parser!(OsString)) .override_usage("cargo afl [SUBCOMMAND or Cargo SUBCOMMAND]") - .after_help(help) + .after_help(HELP) .subcommand( Command::new("addseeds") .about("Invoke afl-addseeds") @@ -316,18 +321,48 @@ where // `-C codegen-units=1` is needed to work around link errors // https://github.com/rust-fuzz/afl.rs/pull/193#issuecomment-933550430 + + let binding = common::afl_llvm_dir(None); + let p = binding.display(); + let mut rustflags = format!( "-C debug-assertions \ - -C overflow_checks \ - -C passes={passes} \ - -C codegen-units=1 \ - -C llvm-args=-sanitizer-coverage-level=3 \ - -C llvm-args=-sanitizer-coverage-trace-pc-guard \ - -C llvm-args=-sanitizer-coverage-prune-blocks=0 \ - -C llvm-args=-sanitizer-coverage-trace-compares \ - -C opt-level=3 \ - -C target-cpu=native " + -C overflow_checks \ + -C passes={passes} \ + -C codegen-units=1 \ + -C opt-level=3 \ + -C target-cpu=native " ); + let mut environment_variables = HashMap::<&str, String>::new(); + environment_variables.insert("ASAN_OPTIONS", asan_options); + environment_variables.insert("TSAN_OPTIONS", tsan_options); + + if cfg!(feature = "plugins") { + // Make sure we are on nightly for the -Z flags + assert!( + rustc_version::version_meta().unwrap().channel == rustc_version::Channel::Nightly, + "cargo-afl must be compiled with nightly for CMPLOG and other advanced AFL++ features" + ); + + rustflags.push_str(&format!( + "-Z llvm-plugins={p}/cmplog-instructions-pass.so \ + -Z llvm-plugins={p}/cmplog-routines-pass.so \ + -Z llvm-plugins={p}/cmplog-switches-pass.so \ + -Z llvm-plugins={p}/SanitizerCoveragePCGUARD.so \ + -Z llvm-plugins={p}/afl-llvm-dict2file.so + " + )); + + environment_variables.insert("AFL_QUIET", "1".to_string()); + } else { + rustflags.push_str( + "-C llvm-args=-sanitizer-coverage-level=3 \ + -C llvm-args=-sanitizer-coverage-trace-pc-guard \ + -C llvm-args=-sanitizer-coverage-prune-blocks=0 \ + -C llvm-args=-sanitizer-coverage-trace-compares + ", + ); + } let no_cfg_fuzzing = env::var("AFL_NO_CFG_FUZZING").is_ok(); if no_cfg_fuzzing { @@ -353,20 +388,20 @@ where rustflags.push_str(&format!( "-l afl-llvm-rt \ -L {} ", - common::afl_llvm_rt_dir(None).display() + common::afl_llvm_dir(None).display() )); // add user provided flags rustflags.push_str(&env::var("RUSTFLAGS").unwrap_or_default()); rustdocflags.push_str(&env::var("RUSTDOCFLAGS").unwrap_or_default()); + environment_variables.insert("RUSTFLAGS", rustflags); + environment_variables.insert("RUSTDOCFLAGS", rustdocflags); + let status = Command::new(cargo_path) .arg(subcommand) .args(args) - .env("RUSTFLAGS", &rustflags) - .env("RUSTDOCFLAGS", &rustdocflags) - .env("ASAN_OPTIONS", asan_options) - .env("TSAN_OPTIONS", tsan_options) + .envs(&environment_variables) .status() .unwrap(); process::exit(status.code().unwrap_or(1)); diff --git a/cargo-afl/src/common.rs b/cargo-afl/src/common.rs index 4170a8194..bbb61f677 100644 --- a/cargo-afl/src/common.rs +++ b/cargo-afl/src/common.rs @@ -57,18 +57,18 @@ pub fn afl_dir(base: Option<&Path>) -> PathBuf { #[allow(dead_code)] #[must_use] -pub fn afl_llvm_rt_dir(base: Option<&Path>) -> PathBuf { - data_dir(base, "afl-llvm-rt") +pub fn afl_llvm_dir(base: Option<&Path>) -> PathBuf { + data_dir(base, "afl-llvm") } #[allow(dead_code)] #[must_use] pub fn object_file_path(base: Option<&Path>) -> PathBuf { - afl_llvm_rt_dir(base).join("libafl-llvm-rt.o") + afl_llvm_dir(base).join("libafl-llvm-rt.o") } #[allow(dead_code)] #[must_use] pub fn archive_file_path(base: Option<&Path>) -> PathBuf { - afl_llvm_rt_dir(base).join("libafl-llvm-rt.a") + afl_llvm_dir(base).join("libafl-llvm-rt.a") }