Skip to content

Commit

Permalink
Don't panic in config::config
Browse files Browse the repository at this point in the history
  • Loading branch information
smoelius committed Mar 10, 2024
1 parent 462eff5 commit 5c2a9bd
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 95 deletions.
9 changes: 7 additions & 2 deletions cargo-afl/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ fn main() {

// smoelius: Build AFLplusplus only when installing and not building on docs.rs.
if installing && !building_on_docs_rs {
config::config(&config::Args {
if let Err(error) = config::config(&config::Args {
build: true,
force: true,
plugins: cfg!(feature = "plugins"),
..Default::default()
});
}) {
println!(
"cargo:warn=Could not build AFLplusplus; it will need to be built manually with \
`cargo afl config --build`: {error}"
);
}
}
}
37 changes: 19 additions & 18 deletions cargo-afl/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#![deny(clippy::expect_used, clippy::panic, clippy::unwrap_used)]

use std::env;
use std::io::{Error, Result};
use std::path::{Path, PathBuf};

fn xdg_dir() -> xdg::BaseDirectories {
fn xdg_dir() -> Result<xdg::BaseDirectories> {
let afl_rustc_version = afl_rustc_version()?;
let prefix = Path::new("afl.rs")
.join(afl_rustc_version())
.join(afl_rustc_version)
.join(pkg_version());
xdg::BaseDirectories::with_prefix(prefix).unwrap()
xdg::BaseDirectories::with_prefix(prefix).map_err(Error::other)
}

fn data_dir(dir_name: &str) -> PathBuf {
fn data_dir(dir_name: &str) -> Result<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,
Expand All @@ -17,21 +21,22 @@ fn data_dir(dir_name: &str) -> PathBuf {
// place their generated files at OUT_DIR too, but we
// don't change that for now for normal builds.
// smoelius: AFL++ is no longer built on docs.rs.
xdg_dir().create_data_directory(dir_name).unwrap()
let xdg_dir = xdg_dir()?;
xdg_dir.create_data_directory(dir_name)
}

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() -> Result<String> {
let version_meta = rustc_version::version_meta().map_err(Error::other)?;
let mut ret = String::from("rustc-");
ret.push_str(&version_meta.semver.to_string());
if let Some(commit_hash) = version_meta.commit_hash {
ret.push('-');
ret.push_str(&commit_hash[..SHORT_COMMIT_HASH_LEN]);
}
ret
Ok(ret)
}

fn pkg_version() -> String {
Expand All @@ -45,25 +50,21 @@ fn pkg_version() -> String {
}

#[allow(dead_code)]
#[must_use]
pub fn afl_dir() -> PathBuf {
pub fn afl_dir() -> Result<PathBuf> {
data_dir("afl")
}

#[allow(dead_code)]
#[must_use]
pub fn afl_llvm_dir() -> PathBuf {
pub fn afl_llvm_dir() -> Result<PathBuf> {
data_dir("afl-llvm")
}

#[allow(dead_code)]
#[must_use]
pub fn object_file_path() -> PathBuf {
afl_llvm_dir().join("libafl-llvm-rt.o")
pub fn object_file_path() -> Result<PathBuf> {
afl_llvm_dir().map(|path| path.join("libafl-llvm-rt.o"))
}

#[allow(dead_code)]
#[must_use]
pub fn archive_file_path() -> PathBuf {
afl_llvm_dir().join("libafl-llvm-rt.a")
pub fn archive_file_path() -> Result<PathBuf> {
afl_llvm_dir().map(|path| path.join("libafl-llvm-rt.a"))
}
178 changes: 110 additions & 68 deletions cargo-afl/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#![deny(clippy::expect_used, clippy::panic, clippy::unwrap_used)]

use clap::Parser;
use std::ffi::OsStr;
use std::io::{Error, Result};
use std::path::Path;
use std::process::{self, Command, Stdio};
use std::process::{Command, ExitStatus, Stdio};

use super::common;

Expand Down Expand Up @@ -33,70 +36,77 @@ pub struct Args {
pub verbose: bool,
}

pub fn config(args: &Args) {
if !args.force && common::archive_file_path().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);
pub fn config(args: &Args) -> Result<()> {
let archive_file_path = common::archive_file_path()?;
if !args.force && archive_file_path.exists() {
let version = common::afl_rustc_version()?;
return Err(Error::other(format!(
"AFL LLVM runtime was already built for Rust {version}; run `cargo afl config --build \
--force` to rebuild it."
)));
}

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();
let tempdir = tempfile::tempdir()?;

if afl_src_dir.join(".git").is_dir() {
let status = Command::new("git")
let success = Command::new("git")
.args(["clone", afl_src_dir_str, &*tempdir.path().to_string_lossy()])
.status()
.expect("could not run 'git'");
assert!(status.success());
.as_ref()
.map_or(false, ExitStatus::success);
if !success {
return Err(Error::other("could not run 'git'"));
}
} else {
fs_extra::dir::copy(
let _: u64 = fs_extra::dir::copy(
afl_src_dir,
tempdir.path(),
&fs_extra::dir::CopyOptions {
content_only: true,
..Default::default()
},
)
.unwrap();
.map_err(Error::other)?;
}

let work_dir = tempdir.path();

build_afl(args, work_dir);
build_afl_llvm_runtime(args, work_dir);
build_afl(args, work_dir)?;
build_afl_llvm_runtime(args, work_dir)?;

if args.plugins {
copy_afl_llvm_plugins(args, work_dir);
copy_afl_llvm_plugins(args, work_dir)?;
}

eprintln!(
"Artifacts written to {}",
common::afl_dir().parent().unwrap().display()
);
let afl_dir = common::afl_dir()?;
let Some(dir) = afl_dir.parent().map(Path::to_path_buf) else {
return Err(Error::other("could not get afl dir parent"));
};
eprintln!("Artifacts written to {}", dir.display());

Ok(())
}

fn build_afl(args: &Args, work_dir: &Path) {
fn build_afl(args: &Args, work_dir: &Path) -> Result<()> {
// 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 afl_dir = common::afl_dir()?;
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())
.env("DESTDIR", afl_dir)
.env("PREFIX", "")
.env_remove("DEBUG");

if args.plugins {
let llvm_config = check_llvm_and_get_config();
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
Expand All @@ -109,60 +119,75 @@ fn build_afl(args: &Args, work_dir: &Path) {
command.stderr(Stdio::null());
}

let status = command.status().expect("could not run 'make install'");
assert!(status.success());
let success = command.status().as_ref().map_or(false, ExitStatus::success);
if !success {
return Err(Error::other("could not run 'make install"));
}

Ok(())
}

fn build_afl_llvm_runtime(args: &Args, work_dir: &Path) {
std::fs::copy(
work_dir.join("afl-compiler-rt.o"),
common::object_file_path(),
)
.expect("Couldn't copy object file");
fn build_afl_llvm_runtime(args: &Args, work_dir: &Path) -> Result<()> {
let object_file_path = common::object_file_path()?;
let _: u64 = std::fs::copy(work_dir.join("afl-compiler-rt.o"), &object_file_path)
.map_err(|error| Error::other(format!("could not copy object file: {error}")))?;

let archive_file_path = common::archive_file_path()?;
let mut command = Command::new(AR_CMD);
command
.arg("r")
.arg(common::archive_file_path())
.arg(common::object_file_path());
.arg(archive_file_path)
.arg(object_file_path);

if !args.verbose {
command.stdout(Stdio::null());
command.stderr(Stdio::null());
}

let status = command.status().expect("could not run 'ar'");
assert!(status.success());
let success = command.status().as_ref().map_or(false, ExitStatus::success);
if !success {
return Err(Error::other("could not run 'ar'"));
}

Ok(())
}

fn copy_afl_llvm_plugins(_args: &Args, work_dir: &Path) {
fn copy_afl_llvm_plugins(_args: &Args, work_dir: &Path) -> Result<()> {
// Iterate over the files in the directory.
for result in work_dir.read_dir().unwrap() {
let entry = result.unwrap();
for result in work_dir.read_dir()? {
let entry = result?;
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().join(&file_name),
)
.unwrap_or_else(|error| {
panic!("Couldn't copy shared object file {file_name:?}: {error}")
});
let afl_llvm_dir = common::afl_llvm_dir()?;
let _: u64 = std::fs::copy(work_dir.join(&file_name), afl_llvm_dir.join(&file_name))
.map_err(|error| {
Error::other(format!(
"could not copy shared object file {file_name:?}: {error}"
))
})?;
}
}

Ok(())
}

fn check_llvm_and_get_config() -> String {
fn check_llvm_and_get_config() -> Result<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();
let version_meta = rustc_version::version_meta().map_err(Error::other)?;
if version_meta.channel != rustc_version::Channel::Nightly {
return Err(Error::other(
"cargo-afl must be compiled with nightly for the plugins feature",
));
}
let Some(llvm_version) = version_meta
.llvm_version
.map(|llvm_version| llvm_version.major.to_string())
else {
return Err(Error::other("could not get llvm version"));
};

// 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
Expand All @@ -173,19 +198,36 @@ fn check_llvm_and_get_config() -> String {
};

// check if llvm tools are installed and with the good version for the plugin compilation
let mut command = Command::new(llvm_config.clone());
let mut command = Command::new(&llvm_config);
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
let out = match command.output() {
Ok(out) => out,
Err(error) => {
return Err(Error::other(format!(
"could not run {llvm_config} --version: {error}"
)));
}
};

let version = match String::from_utf8(out.stdout) {
Ok(version) => version,
Err(error) => {
return Err(Error::other(format!(
"could not convert {llvm_config} --version output to utf8: {error}"
)));
}
};
let Some(major) = version.split('.').next() else {
return Err(Error::other(format!(
"could not get major from {llvm_config} --version output",
)));
};
if major != llvm_version {
return Err(Error::other(format!(
"{llvm_config} --version output does not contain expected major version \
({llvm_version})",
)));
}

Ok(llvm_config)
}
Loading

0 comments on commit 5c2a9bd

Please sign in to comment.