diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4d2b46c5d..a3235c950 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++ @@ -71,13 +71,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 --build $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..27d0f719d 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::config(&config::Args { + build: true, + force: true, + plugins: cfg!(feature = "plugins"), + ..Default::default() + }); } } diff --git a/cargo-afl/src/config.rs b/cargo-afl/src/config.rs new file mode 100644 index 000000000..2f483b487 --- /dev/null +++ b/cargo-afl/src/config.rs @@ -0,0 +1,191 @@ +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)] +#[clap(after_help = "\ +If you are using rustup, you can build AFL++ for a specific TOOLCHAIN as follows: + + cargo +TOOLCHAIN afl config --build")] +pub struct Args { + #[clap(long, help = "Build AFL++ for the default toolchain")] + pub build: bool, + + #[clap(long, help = "Rebuild AFL++ if it was already built")] + pub force: bool, + + #[clap(long, help = "Enable building of LLVM plugins")] + pub plugins: bool, + + #[clap(long, help = "Show build output")] + pub verbose: bool, +} + +pub fn config(args: &Args) { + if !args.force && common::archive_file_path(None).exists() { + let version = common::afl_rustc_version(); + eprintln!( + "AFL LLVM runtime was already built for Rust {version}; run `cargo \ + afl config --build --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).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++ + // smoelius: AFL++ is now copied to a temporary directory before being built. So `make clean` + // is no longer necessary. + let mut command = Command::new("make"); + command + .current_dir(work_dir) + .arg("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 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` much 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 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), + ) + .expect("Couldn't copy object file"); + + let mut command = Command::new(AR_CMD); + command + .arg("r") + .arg(common::archive_file_path(base)) + .arg(common::object_file_path(base)); + + if !args.verbose { + command.stdout(Stdio::null()); + command.stderr(Stdio::null()); + } + + let status = command.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).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 90% rename from cargo-afl/src/bin/cargo-afl.rs rename to cargo-afl/src/main.rs index 0bcbed4d2..2f12033a4 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).exists() + { + let version = common::afl_rustc_version(); + eprintln!( + "AFL LLVM runtime was not built for Rust {version}; run `cargo \ + afl config --build` 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::config(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 }) => { @@ -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, @@ -313,6 +331,18 @@ fn is_nightly() -> bool { .success() } +fn plugins_available() -> bool { + let afl_llvm_dir = common::afl_llvm_dir(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..7fbe1a825 100644 --- a/cargo-afl/tests/crates_io.rs +++ b/cargo-afl/tests/crates_io.rs @@ -73,31 +73,53 @@ 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", "--build"]) + .env("HOME", temp_home.path()) + .assert() + .failure() + .stderr( + predicates::str::is_match( + "AFL LLVM runtime was already built for Rust [^;]*; run `cargo \ + afl config --build --force` to rebuild it\\.", + ) + .unwrap(), + ); + + Command::new(cargo_afl) + .args(["afl", "config", "--build", "--force"]) + .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();