From 7f207915d802061f78b4d0a30498aa1c01ea8a9d Mon Sep 17 00:00:00 2001 From: Samuel Moelius Date: Fri, 17 Nov 2023 06:37:08 -0500 Subject: [PATCH] Add `config` subcommand --- .github/workflows/rust.yml | 20 +- cargo-afl/Cargo.toml | 4 + cargo-afl/build.rs | 193 ++----------------- cargo-afl/src/common.rs | 46 +++-- cargo-afl/src/config.rs | 199 ++++++++++++++++++++ cargo-afl/src/{bin/cargo-afl.rs => main.rs} | 80 +++++--- cargo-afl/tests/crates_io.rs | 68 ++++++- 7 files changed, 377 insertions(+), 233 deletions(-) create mode 100644 cargo-afl/src/config.rs rename cargo-afl/src/{bin/cargo-afl.rs => main.rs} (89%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4d2b46c5d..ca62a419e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,13 +34,13 @@ jobs: matrix: environment: [ubuntu-latest, macos-latest] toolchain: [stable, nightly] - features: [default, plugins] + plugins: [true, false] cc: [cc, clang] exclude: - environment: macos-latest - features: plugins + plugins: true - toolchain: stable - features: plugins + plugins: true include: - cc: cc cxx: c++ @@ -56,6 +56,8 @@ jobs: submodules: true - name: Rustup run: rustup default ${{ matrix.toolchain }} + - name: Install beta toolchain (for testing) + run: rustup toolchain install beta - name: Install LLVM run: | LLVM_VERSION="$(rustc --version -v | grep '^LLVM version:' | grep -o '[0-9]\+' | head -n 1)" @@ -71,13 +73,17 @@ jobs: env: HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 - name: Build - run: cargo build --features=${{ matrix.features }} -vv + run: cargo build -vv + - name: Run `cargo afl config` + run: | + PLUGINS="$(${{ matrix.plugins }} && echo --plugins)" || true + cargo run -- afl config --default $PLUGINS - name: Run afl-system-config - run: cargo run --features=${{ matrix.features }} -- afl system-config + run: cargo run -- afl system-config - name: Build examples (with AFL instrumentation) - run: cargo run --features=${{ matrix.features }} -- afl build --examples -vv + run: cargo run -- afl build --examples -vv - name: Run tests - run: cargo test --features=${{ matrix.features }} -p cargo-afl -vv + run: cargo test -p cargo-afl -vv all-checks: needs: [lint, build] runs-on: ubuntu-latest diff --git a/cargo-afl/Cargo.toml b/cargo-afl/Cargo.toml index 6553b9fba..44d49ab89 100644 --- a/cargo-afl/Cargo.toml +++ b/cargo-afl/Cargo.toml @@ -14,6 +14,7 @@ homepage = "https://github.com/rust-fuzz/afl.rs" edition = "2021" [build-dependencies] +clap = { version = "4.4", features = ["cargo", "derive"] } fs_extra = "1.3" home = "0.5" libc = "0.2" @@ -23,8 +24,11 @@ xdg = "2.5" [dependencies] clap = { version = "4.4", features = ["cargo", "derive"] } +fs_extra = "1.3" +home = "0.5" libc = "0.2" rustc_version = "0.4" +tempfile = "3.8" xdg = "2.5" [dev-dependencies] diff --git a/cargo-afl/build.rs b/cargo-afl/build.rs index aef1bb03b..9dc76c283 100644 --- a/cargo-afl/build.rs +++ b/cargo-afl/build.rs @@ -1,19 +1,12 @@ use std::env; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::process::Command; - -static AFL_SRC_PATH: &str = "AFLplusplus"; - -// https://github.com/rust-fuzz/afl.rs/issues/148 -#[cfg(target_os = "macos")] -static AR_CMD: &str = "/usr/bin/ar"; -#[cfg(not(target_os = "macos"))] -static AR_CMD: &str = "ar"; +use std::path::Path; #[path = "src/common.rs"] mod common; +#[path = "src/config.rs"] +mod config; + fn main() { let installing = home::cargo_home() .map(|path| Path::new(env!("CARGO_MANIFEST_DIR")).starts_with(path)) @@ -22,175 +15,13 @@ fn main() { let building_on_docs_rs = env::var("DOCS_RS").is_ok(); - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - - // smoelius: Build AFLplusplus in a temporary directory when installing or when building on docs.rs. - let work_dir = if installing || building_on_docs_rs { - let tempdir = tempfile::tempdir_in(&out_dir).unwrap(); - if Path::new(AFL_SRC_PATH).join(".git").is_dir() { - let status = Command::new("git") - .args(["clone", AFL_SRC_PATH, &*tempdir.path().to_string_lossy()]) - .status() - .expect("could not run 'git'"); - assert!(status.success()); - } else { - fs_extra::dir::copy( - AFL_SRC_PATH, - tempdir.path(), - &fs_extra::dir::CopyOptions { - content_only: true, - ..Default::default() - }, - ) - .unwrap(); - } - tempdir.into_path() - } else { - PathBuf::from(AFL_SRC_PATH) - }; - - let base = if building_on_docs_rs { - Some(out_dir) - } else { - None - }; - - // smoelius: Lock `work_dir` until the build script exits. - #[cfg(unix)] - let _file = sys::lock_path(&work_dir).unwrap(); - - 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", "install"]) - // skip the checks for the legacy x86 afl-gcc compiler - .env("AFL_NO_X86", "1") - .env("DESTDIR", common::afl_dir(base)) - .env("PREFIX", "") - .env_remove("DEBUG"); - - if cfg!(feature = "plugins") { - let llvm_config = check_llvm_and_get_config(); - command.env("LLVM_CONFIG", llvm_config); - } else { - // build just the runtime to avoid troubles with Xcode clang on macOS - // smoelius: `NO_BUILD=1` also makes `cargo build` significantly faster. - command.env("NO_BUILD", "1"); - } - - let status = command - .status() - .expect("could not run 'make clean install'"); - assert!(status.success()); -} - -fn build_afl_llvm_runtime(work_dir: &Path, base: Option<&Path>) { - std::fs::copy( - work_dir.join("afl-compiler-rt.o"), - common::object_file_path(base), - ) - .expect("Couldn't copy object file"); - - let status = Command::new(AR_CMD) - .arg("r") - .arg(common::archive_file_path(base)) - .arg(common::object_file_path(base)) - .status() - .expect("could not run 'ar'"); - assert!(status.success()); -} - -fn copy_afl_llvm_plugins(work_dir: &Path, base: Option<&Path>) { - // Iterate over the files in the directory. - for result in work_dir.read_dir().unwrap() { - let entry = result.unwrap(); - let file_name = entry.file_name(); - - // Get the file extension. Only copy the files that are shared objects. - if Path::new(&file_name).extension() == Some(OsStr::new("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(|error| { - panic!("Couldn't copy shared object file {file_name:?}: {error}") - }); - } - } -} - -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; - use std::io::{Error, Result}; - use std::os::unix::io::AsRawFd; - use std::path::Path; - - pub fn lock_path(path: &Path) -> Result { - let file = File::open(path)?; - lock_exclusive(&file)?; - Ok(file) - } - - // smoelius: `lock_exclusive` and `flock` were copied from: - // https://github.com/rust-lang/cargo/blob/ae91d4ed41da98bdfa16041dbc6cd30287920120/src/cargo/util/flock.rs - - fn lock_exclusive(file: &File) -> Result<()> { - flock(file, libc::LOCK_EX) - } - - fn flock(file: &File, flag: libc::c_int) -> Result<()> { - let ret = unsafe { libc::flock(file.as_raw_fd(), flag) }; - if ret < 0 { - Err(Error::last_os_error()) - } else { - Ok(()) - } + // smoelius: Build AFLplusplus only when installing and not building on docs.rs. + if installing && !building_on_docs_rs { + config::build(&config::Args { + default: true, + force: true, + plugins: cfg!(feature = "plugins"), + ..Default::default() + }); } } diff --git a/cargo-afl/src/common.rs b/cargo-afl/src/common.rs index bbb61f677..1168c7081 100644 --- a/cargo-afl/src/common.rs +++ b/cargo-afl/src/common.rs @@ -1,14 +1,15 @@ use std::env; use std::path::{Path, PathBuf}; +use std::process::Command; -fn xdg_dir() -> xdg::BaseDirectories { +fn xdg_dir(toolchain: Option<&str>) -> xdg::BaseDirectories { let prefix = Path::new("afl.rs") - .join(afl_rustc_version()) + .join(afl_rustc_version(toolchain)) .join(pkg_version()); xdg::BaseDirectories::with_prefix(prefix).unwrap() } -fn data_dir(base: Option<&Path>, dir_name: &str) -> PathBuf { +fn data_dir(base: Option<&Path>, toolchain: Option<&str>, dir_name: &str) -> PathBuf { // For docs.rs builds, use OUT_DIR. // For other cases, use a XDG data directory. // It is necessary to use OUT_DIR for docs.rs builds, @@ -21,15 +22,20 @@ fn data_dir(base: Option<&Path>, dir_name: &str) -> PathBuf { std::fs::create_dir_all(&path).unwrap(); path } else { - xdg_dir().create_data_directory(dir_name).unwrap() + xdg_dir(toolchain).create_data_directory(dir_name).unwrap() } } const SHORT_COMMIT_HASH_LEN: usize = 7; #[must_use] -pub fn afl_rustc_version() -> String { - let version_meta = rustc_version::version_meta().unwrap(); +pub fn afl_rustc_version(toolchain: Option<&str>) -> String { + let version_meta = toolchain + .map_or_else( + rustc_version::version_meta, + rustc_version_meta_for_toolchain, + ) + .unwrap(); let mut ret = String::from("rustc-"); ret.push_str(&version_meta.semver.to_string()); if let Some(commit_hash) = version_meta.commit_hash { @@ -39,6 +45,18 @@ pub fn afl_rustc_version() -> String { ret } +fn rustc_version_meta_for_toolchain( + toolchain: &str, +) -> Result { + let output = Command::new("rustc") + .args([format!("+{toolchain}").as_str(), "-vV"]) + .output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + rustc_version::version_meta_for(&stdout) +} + fn pkg_version() -> String { let mut ret = String::from("afl.rs-"); @@ -51,24 +69,24 @@ fn pkg_version() -> String { #[allow(dead_code)] #[must_use] -pub fn afl_dir(base: Option<&Path>) -> PathBuf { - data_dir(base, "afl") +pub fn afl_dir(base: Option<&Path>, toolchain: Option<&str>) -> PathBuf { + data_dir(base, toolchain, "afl") } #[allow(dead_code)] #[must_use] -pub fn afl_llvm_dir(base: Option<&Path>) -> PathBuf { - data_dir(base, "afl-llvm") +pub fn afl_llvm_dir(base: Option<&Path>, toolchain: Option<&str>) -> PathBuf { + data_dir(base, toolchain, "afl-llvm") } #[allow(dead_code)] #[must_use] -pub fn object_file_path(base: Option<&Path>) -> PathBuf { - afl_llvm_dir(base).join("libafl-llvm-rt.o") +pub fn object_file_path(base: Option<&Path>, toolchain: Option<&str>) -> PathBuf { + afl_llvm_dir(base, toolchain).join("libafl-llvm-rt.o") } #[allow(dead_code)] #[must_use] -pub fn archive_file_path(base: Option<&Path>) -> PathBuf { - afl_llvm_dir(base).join("libafl-llvm-rt.a") +pub fn archive_file_path(base: Option<&Path>, toolchain: Option<&str>) -> PathBuf { + afl_llvm_dir(base, toolchain).join("libafl-llvm-rt.a") } diff --git a/cargo-afl/src/config.rs b/cargo-afl/src/config.rs new file mode 100644 index 000000000..1e30eb39b --- /dev/null +++ b/cargo-afl/src/config.rs @@ -0,0 +1,199 @@ +use clap::Parser; +use std::ffi::OsStr; +use std::path::Path; +use std::process::{self, Command, Stdio}; + +use super::common; + +const AFL_SRC_PATH: &str = "AFLplusplus"; + +// https://github.com/rust-fuzz/afl.rs/issues/148 +#[cfg(target_os = "macos")] +static AR_CMD: &str = "/usr/bin/ar"; +#[cfg(not(target_os = "macos"))] +static AR_CMD: &str = "ar"; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Default, Parser)] +pub struct Args { + #[clap( + long, + help = "Build AFL++ for the default toolchain", + conflicts_with = "toolchain", + required_unless_present = "toolchain" + )] + pub default: bool, + + #[clap(long, help = "Rebuild AFL++ if it was already built")] + pub force: bool, + + #[clap(long, help = "Build AFL++ with LLVM plugins")] + pub plugins: bool, + + #[clap(long, help = "Show build output")] + pub verbose: bool, + + #[clap( + long, + help = "Build AFL++ for ", + conflicts_with = "default", + required_unless_present = "default" + )] + pub toolchain: Option, +} + +pub fn build(args: &Args) { + assert!(args.default || args.toolchain.is_some()); + + if !args.force && common::archive_file_path(None, args.toolchain.as_deref()).exists() { + let version = common::afl_rustc_version(args.toolchain.as_deref()); + eprintln!( + "AFL LLVM runtime was already built for Rust {version}; run `cargo \ + afl config --force` to rebuild it." + ); + process::exit(1); + } + + let afl_src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(AFL_SRC_PATH); + let afl_src_dir_str = &afl_src_dir.to_string_lossy(); + + let tempdir = tempfile::tempdir().unwrap(); + + if afl_src_dir.join(".git").is_dir() { + let status = Command::new("git") + .args(["clone", afl_src_dir_str, &*tempdir.path().to_string_lossy()]) + .status() + .expect("could not run 'git'"); + assert!(status.success()); + } else { + fs_extra::dir::copy( + afl_src_dir, + tempdir.path(), + &fs_extra::dir::CopyOptions { + content_only: true, + ..Default::default() + }, + ) + .unwrap(); + } + + let work_dir = tempdir.path(); + + build_afl(args, work_dir, None); + build_afl_llvm_runtime(args, work_dir, None); + + if args.plugins { + copy_afl_llvm_plugins(args, work_dir, None); + } + + eprintln!( + "Artifacts written to {}", + common::afl_dir(None, args.toolchain.as_deref()) + .parent() + .unwrap() + .display() + ); +} + +fn build_afl(args: &Args, 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", "install"]) + // skip the checks for the legacy x86 afl-gcc compiler + .env("AFL_NO_X86", "1") + .env("DESTDIR", common::afl_dir(base, args.toolchain.as_deref())) + .env("PREFIX", "") + .env_remove("DEBUG"); + + if args.plugins { + let llvm_config = check_llvm_and_get_config(); + command.env("LLVM_CONFIG", llvm_config); + } else { + // build just the runtime to avoid troubles with Xcode clang on macOS + // smoelius: `NO_BUILD=1` also makes `cargo build` significantly faster. + command.env("NO_BUILD", "1"); + } + + if !args.verbose { + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + } + + let status = command + .status() + .expect("could not run 'make clean install'"); + assert!(status.success()); +} + +fn build_afl_llvm_runtime(args: &Args, work_dir: &Path, base: Option<&Path>) { + std::fs::copy( + work_dir.join("afl-compiler-rt.o"), + common::object_file_path(base, args.toolchain.as_deref()), + ) + .expect("Couldn't copy object file"); + + let status = Command::new(AR_CMD) + .arg("r") + .arg(common::archive_file_path(base, args.toolchain.as_deref())) + .arg(common::object_file_path(base, args.toolchain.as_deref())) + .status() + .expect("could not run 'ar'"); + assert!(status.success()); +} + +fn copy_afl_llvm_plugins(args: &Args, work_dir: &Path, base: Option<&Path>) { + // Iterate over the files in the directory. + for result in work_dir.read_dir().unwrap() { + let entry = result.unwrap(); + let file_name = entry.file_name(); + + // Get the file extension. Only copy the files that are shared objects. + if Path::new(&file_name).extension() == Some(OsStr::new("so")) { + // Attempt to copy the shared object file. + std::fs::copy( + work_dir.join(&file_name), + common::afl_llvm_dir(base, args.toolchain.as_deref()).join(&file_name), + ) + .unwrap_or_else(|error| { + panic!("Couldn't copy shared object file {file_name:?}: {error}") + }); + } + } +} + +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 +} diff --git a/cargo-afl/src/bin/cargo-afl.rs b/cargo-afl/src/main.rs similarity index 89% rename from cargo-afl/src/bin/cargo-afl.rs rename to cargo-afl/src/main.rs index 0bcbed4d2..3bfd10aea 100644 --- a/cargo-afl/src/bin/cargo-afl.rs +++ b/cargo-afl/src/main.rs @@ -1,21 +1,16 @@ -use clap::Parser; +use clap::{crate_version, Parser}; use std::collections::HashMap; use std::env; use std::ffi::{OsStr, OsString}; +use std::path::Path; use std::process::{self, Command, Stdio}; -#[path = "../common.rs"] mod common; +mod config; 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") -}; - #[derive(Parser)] #[clap( display_name = "cargo", @@ -34,7 +29,7 @@ enum CargoSubcommand { #[derive(Parser)] #[clap( - version = VERSION, + version = crate_version!(), allow_hyphen_values = true, arg_required_else_help = true, override_usage = "cargo afl [SUBCOMMAND or Cargo SUBCOMMAND]", @@ -59,7 +54,24 @@ macro_rules! construct_afl_subcommand_variants { $($constructed_variants)* } }; - // inductive case + // inductive case, with args type + ( + { + $($constructed_variants:tt)* + } $variant:ident ( $about:literal, $args_ty:ty ), $($unused_materials:tt)* + ) => { + construct_afl_subcommand_variants! { + { + $($constructed_variants)* + #[clap( + about = $about, + arg_required_else_help = true, + )] + $variant($args_ty), + } $($unused_materials)* + } + }; + // inductive case, without args type ( { $($constructed_variants:tt)* @@ -93,6 +105,7 @@ declare_afl_subcommand_enum! { Addseeds("Invoke afl-addseeds"), Analyze("Invoke afl-analyze"), Cmin("Invoke afl-cmin"), + Config("Build or rebuild AFL++", config::Args), Fuzz("Invoke afl-fuzz"), Gotcpu("Invoke afl-gotcpu"), Plot("Invoke afl-plot"), @@ -103,35 +116,40 @@ declare_afl_subcommand_enum! { } fn main() { - if !common::archive_file_path(None).exists() { - let version = common::afl_rustc_version(); - eprintln!( - "AFL LLVM runtime is not built with Rust {version}, run `cargo \ - install --force cargo-afl` to build it." - ); - process::exit(1); - } - let afl_args = match Args::parse() { Args { subcmd: CargoSubcommand::Afl(afl_args), } => afl_args, }; - match afl_args.subcmd { + if !matches!(afl_args.subcmd, Some(AflSubcommand::Config(..))) + && !common::archive_file_path(None, None).exists() + { + let version = common::afl_rustc_version(None); + eprintln!( + "AFL LLVM runtime was not built for Rust {version}; run `cargo \ + afl config` to build it." + ); + process::exit(1); + } + + match &afl_args.subcmd { Some(AflSubcommand::Addseeds { args }) => { run_afl("afl-addseeds", args); } Some(AflSubcommand::Analyze { args }) => { run_afl("afl-analyze", args); } + Some(AflSubcommand::Config(args)) => { + config::build(args); + } Some(AflSubcommand::Cmin { args }) => { run_afl("afl-cmin", args); } Some(AflSubcommand::Fuzz { args }) => { // We prepend -c0 to the AFL++ arguments let cmplog_flag = vec![OsString::from("-c0")]; - let args = cmplog_flag.into_iter().chain(args); + let args = cmplog_flag.iter().chain(args); run_afl("afl-fuzz", args); } Some(AflSubcommand::Gotcpu { args }) => { @@ -164,7 +182,7 @@ where S: AsRef, { let no_sudo = env::var("NO_SUDO").is_ok(); - let cmd_path = common::afl_dir(None).join("bin").join(tool); + let cmd_path = common::afl_dir(None, None).join("bin").join(tool); let mut cmd = if !no_sudo && tool == "afl-system-config" { let mut cmd = Command::new("sudo"); cmd.args([OsStr::new("--reset-timestamp"), cmd_path.as_os_str()]); @@ -220,7 +238,7 @@ 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 binding = common::afl_llvm_dir(None, None); let p = binding.display(); let mut rustflags = format!( @@ -235,7 +253,7 @@ where environment_variables.insert("ASAN_OPTIONS", asan_options); environment_variables.insert("TSAN_OPTIONS", tsan_options); - if cfg!(feature = "plugins") { + if plugins_available() { // Make sure we are on nightly for the -Z flags assert!( rustc_version::version_meta().unwrap().channel == rustc_version::Channel::Nightly, @@ -286,7 +304,7 @@ where rustflags.push_str(&format!( "-l afl-llvm-rt \ -L {} ", - common::afl_llvm_dir(None).display() + common::afl_llvm_dir(None, None).display() )); // add user provided flags @@ -313,6 +331,18 @@ fn is_nightly() -> bool { .success() } +fn plugins_available() -> bool { + let afl_llvm_dir = common::afl_llvm_dir(None, None); + for result in afl_llvm_dir.read_dir().unwrap() { + let entry = result.unwrap(); + let file_name = entry.file_name(); + if Path::new(&file_name).extension() == Some(OsStr::new("so")) { + return true; + } + } + false +} + #[cfg(all(test, unix))] mod tests { use super::*; diff --git a/cargo-afl/tests/crates_io.rs b/cargo-afl/tests/crates_io.rs index 8ac9b4144..a97e6ca98 100644 --- a/cargo-afl/tests/crates_io.rs +++ b/cargo-afl/tests/crates_io.rs @@ -73,31 +73,87 @@ fn build() { } #[test] -fn install() { - let tempdir = tempdir().unwrap(); +fn install_and_config() { + let temp_home = tempdir().unwrap(); + let temp_cargo_home = tempdir().unwrap(); - let cargo_afl = tempdir.path().join("bin/cargo-afl"); + let cargo_afl = temp_cargo_home.path().join("bin/cargo-afl"); assert!(!cargo_afl.exists()); Command::new("cargo") .args(["install", "--path", "../cargo-afl"]) - .env("CARGO_HOME", tempdir.path()) + .env("HOME", temp_home.path()) + .env("CARGO_HOME", temp_cargo_home.path()) .env("TESTING_INSTALL", "1") .assert() .success(); - Command::new(cargo_afl) + Command::new(&cargo_afl) .args(["afl", "--help"]) .assert() .success(); + + // smoelius: Verify that `--force` is needed to rebuild AFL++. + Command::new(&cargo_afl) + .args(["afl", "config", "--default"]) + .env("HOME", temp_home.path()) + .assert() + .failure() + .stderr( + predicates::str::is_match( + "AFL LLVM runtime was already built for Rust [^;]*; run `cargo \ + afl config --force` to rebuild it\\.", + ) + .unwrap(), + ); + + Command::new(cargo_afl) + .args(["afl", "config", "--default", "--force"]) + .env("HOME", temp_home.path()) + .assert() + .success(); +} + +#[test] +fn config_with_toolchain() { + let temp_home = tempdir().unwrap(); + + Command::cargo_bin("cargo-afl") + .unwrap() + .args(["afl", "config", "--default"]) + .env("HOME", temp_home.path()) + .assert() + .success(); + + Command::cargo_bin("cargo-afl") + .unwrap() + .args(["afl", "config", "--default"]) + .env("HOME", temp_home.path()) + .assert() + .failure() + .stderr( + predicates::str::is_match( + "AFL LLVM runtime was already built for Rust [^;]*; run `cargo \ + afl config --force` to rebuild it\\.", + ) + .unwrap(), + ); + + // smoelius: CI tests with stable and nightly, but not beta. So the next command should succeed. + Command::cargo_bin("cargo-afl") + .unwrap() + .args(["afl", "config", "--toolchain", "beta"]) + .env("HOME", temp_home.path()) + .assert() + .success(); } #[test] fn publish() { for subdir in ["afl", "cargo-afl"] { Command::new("cargo") - .args(["publish", "--allow-dirty", "--dry-run", "--no-verify"]) + .args(["publish", "--allow-dirty", "--dry-run"]) .current_dir(Path::new("..").join(subdir)) .assert() .success();