Skip to content

Commit 7e839a9

Browse files
committed
Change how AFL++ source is managed
1 parent bab64b4 commit 7e839a9

File tree

3 files changed

+152
-25
lines changed

3 files changed

+152
-25
lines changed

cargo-afl/src/common.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ pub fn archive_file_path() -> Result<PathBuf> {
6262
afl_llvm_dir().map(|path| path.join("libafl-llvm-rt.a"))
6363
}
6464

65+
pub fn aflplusplus_dir() -> Result<PathBuf> {
66+
aflplusplus_dir_from_base_dir(&xdg_base_dir())
67+
}
68+
69+
/// Construct the AFLplusplus directory from [`xdg::BaseDirectories`]
70+
///
71+
/// This function exists only for tests. Non-test code should use [`aflplusplus_dir`].
72+
pub fn aflplusplus_dir_from_base_dir(base_dir: &xdg::BaseDirectories) -> Result<PathBuf> {
73+
base_dir
74+
.create_data_directory("AFLplusplus")
75+
.map_err(Into::into)
76+
}
77+
6578
pub fn plugins_installed() -> Result<bool> {
6679
let afl_llvm_dir = afl_llvm_dir()?;
6780
for result in afl_llvm_dir

cargo-afl/src/config.rs

Lines changed: 138 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![deny(clippy::disallowed_macros, clippy::expect_used, clippy::unwrap_used)]
2-
31
use anyhow::{Context, Result, bail, ensure};
42
use clap::Parser;
53
use std::ffi::OsStr;
@@ -9,6 +7,7 @@ use std::process::{Command, ExitStatus, Stdio};
97
use super::common;
108

119
const AFL_SRC_PATH: &str = "AFLplusplus";
10+
const AFLPLUSPLUS_URL: &str = "https://github.com/AFLplusplus/AFLplusplus";
1211

1312
// https://github.com/rust-fuzz/afl.rs/issues/148
1413
#[cfg(target_os = "macos")]
@@ -26,60 +25,153 @@ pub struct Args {
2625
#[clap(long, help = "Build AFL++ for the default toolchain")]
2726
pub build: bool,
2827

29-
#[clap(long, help = "Rebuild AFL++ if it was already built")]
28+
#[clap(
29+
long,
30+
help = "Rebuild AFL++ if it was already built. Note: AFL++ will be built without plugins \
31+
if `--plugins` is not passed."
32+
)]
3033
pub force: bool,
3134

3235
#[clap(long, help = "Enable building of LLVM plugins")]
3336
pub plugins: bool,
3437

38+
#[clap(
39+
long,
40+
help = "Update to <TAG> instead of the latest stable version",
41+
requires = "update"
42+
)]
43+
pub tag: Option<String>,
44+
45+
#[clap(
46+
long,
47+
help = "Update AFL++ to the latest stable version (preserving plugins, if applicable)"
48+
)]
49+
pub update: bool,
50+
3551
#[clap(long, help = "Show build output")]
3652
pub verbose: bool,
3753
}
3854

3955
pub fn config(args: &Args) -> Result<()> {
4056
let archive_file_path = common::archive_file_path()?;
41-
if !args.force && archive_file_path.exists() && args.plugins == common::plugins_installed()? {
57+
58+
if !args.force
59+
&& !args.update
60+
&& archive_file_path.exists()
61+
&& args.plugins == common::plugins_installed()?
62+
{
4263
let version = common::afl_rustc_version()?;
4364
bail!(
4465
"AFL LLVM runtime was already built for Rust {version}; run `cargo afl config --build \
4566
--force` to rebuild it."
4667
);
4768
}
4869

49-
let afl_src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(AFL_SRC_PATH);
50-
let afl_src_dir_str = &afl_src_dir.to_string_lossy();
51-
52-
let tempdir = tempfile::tempdir().with_context(|| "could not create temporary directory")?;
70+
// smoelius: If updating and AFL++ was built with plugins before, build with plugins again.
71+
let args = Args {
72+
plugins: if args.update {
73+
common::plugins_installed().is_ok_and(|is_true| is_true)
74+
} else {
75+
args.plugins
76+
},
77+
tag: args.tag.clone(),
78+
..*args
79+
};
5380

54-
if afl_src_dir.join(".git").is_dir() {
55-
let success = Command::new("git")
56-
.args(["clone", afl_src_dir_str, &*tempdir.path().to_string_lossy()])
57-
.status()
58-
.as_ref()
59-
.is_ok_and(ExitStatus::success);
60-
ensure!(success, "could not run 'git'");
61-
} else {
62-
copy_aflplusplus_submodule(&tempdir.path().join(AFL_SRC_PATH))?;
81+
let aflplusplus_dir =
82+
common::aflplusplus_dir().with_context(|| "could not determine AFLplusplus directory")?;
83+
84+
// smoelius: The AFLplusplus directory could be in one of three possible states:
85+
//
86+
// 1. Nonexistent
87+
// 2. Initialized with a copy of the AFLplusplus submodule from afl.rs's source tree
88+
// 3. Cloned from `AFLPLUSPLUS_URL`
89+
//
90+
// If we are not updating and the AFLplusplus directory is nonexistent: initialize the directory
91+
// with a copy of the AFLplusplus submodule from afl.rs's source tree (the `else` case in the
92+
// next `if` statement).
93+
//
94+
// If we are updating and the AFLplusplus directory is a copy of the AFLplusplus submodule from
95+
// afl.rs's source tree: remove it and create a new directory by cloning AFL++ (the `else` case
96+
// in `update_to_stable_or_tag`).
97+
//
98+
// Finally, if we are updating: check out either `origin/stable` or the tag that was passed.
99+
if args.update {
100+
let rev_prev = if is_repo(&aflplusplus_dir)? {
101+
rev(&aflplusplus_dir).map(Some)?
102+
} else {
103+
None
104+
};
105+
106+
update_to_stable_or_tag(&aflplusplus_dir, args.tag.as_deref())?;
107+
108+
let rev_curr = rev(&aflplusplus_dir)?;
109+
110+
if rev_prev == Some(rev_curr) && !args.force {
111+
eprintln!("Nothing to do. Pass `--force` to force rebuilding.");
112+
return Ok(());
113+
}
114+
} else if !aflplusplus_dir.join(".git").try_exists()? {
115+
copy_aflplusplus_submodule(&aflplusplus_dir)?;
63116
}
64117

65-
let work_dir = tempdir.path().join(AFL_SRC_PATH);
66-
67-
build_afl(args, &work_dir)?;
68-
build_afl_llvm_runtime(args, &work_dir)?;
118+
build_afl(&args, &aflplusplus_dir)?;
119+
build_afl_llvm_runtime(&args, &aflplusplus_dir)?;
69120

70121
if args.plugins {
71-
copy_afl_llvm_plugins(args, &work_dir)?;
122+
copy_afl_llvm_plugins(&args, &aflplusplus_dir)?;
72123
}
73124

74125
let afl_dir = common::afl_dir()?;
75-
let Some(dir) = afl_dir.parent().map(Path::to_path_buf) else {
126+
let Some(afl_dir_parent) = afl_dir.parent() else {
76127
bail!("could not get afl dir parent");
77128
};
78-
eprintln!("Artifacts written to {}", dir.display());
129+
eprintln!("Artifacts written to {}", afl_dir_parent.display());
79130

80131
Ok(())
81132
}
82133

134+
fn update_to_stable_or_tag(aflplusplus_dir: &Path, tag: Option<&str>) -> Result<()> {
135+
if is_repo(aflplusplus_dir)? {
136+
let success = Command::new("git")
137+
.arg("fetch")
138+
.current_dir(aflplusplus_dir)
139+
.status()
140+
.as_ref()
141+
.is_ok_and(ExitStatus::success);
142+
ensure!(success, "could not run 'git fetch'");
143+
} else {
144+
remove_aflplusplus_dir(aflplusplus_dir).unwrap_or_default();
145+
let success = Command::new("git")
146+
.args([
147+
"clone",
148+
AFLPLUSPLUS_URL,
149+
&*aflplusplus_dir.to_string_lossy(),
150+
])
151+
.status()
152+
.as_ref()
153+
.is_ok_and(ExitStatus::success);
154+
ensure!(success, "could not run 'git clone'");
155+
}
156+
157+
let mut command = Command::new("git");
158+
command.arg("checkout");
159+
if let Some(tag) = tag {
160+
command.arg(tag);
161+
} else {
162+
command.arg("origin/stable");
163+
}
164+
command.current_dir(aflplusplus_dir);
165+
let success = command.status().as_ref().is_ok_and(ExitStatus::success);
166+
ensure!(success, "could not run 'git checkout'");
167+
168+
Ok(())
169+
}
170+
171+
fn remove_aflplusplus_dir(aflplusplus_dir: &Path) -> Result<()> {
172+
std::fs::remove_dir_all(aflplusplus_dir).map_err(Into::into)
173+
}
174+
83175
fn copy_aflplusplus_submodule(aflplusplus_dir: &Path) -> Result<()> {
84176
let afl_src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(AFL_SRC_PATH);
85177
let afl_src_dir_str = &afl_src_dir.to_string_lossy();
@@ -104,6 +196,28 @@ fn copy_aflplusplus_submodule(aflplusplus_dir: &Path) -> Result<()> {
104196
Ok(())
105197
}
106198

199+
// smoelius: `dot_git` will refer to an ASCII text file if it was copied from the AFLplusplus
200+
// submodule from afl.rs's source tree.
201+
fn is_repo(aflplusplus_dir: &Path) -> Result<bool> {
202+
let dot_git = aflplusplus_dir.join(".git");
203+
if dot_git.try_exists()? {
204+
Ok(dot_git.is_dir())
205+
} else {
206+
Ok(false)
207+
}
208+
}
209+
210+
fn rev(dir: &Path) -> Result<String> {
211+
let mut command = Command::new("git");
212+
command.args(["rev-parse", "HEAD"]);
213+
command.current_dir(dir);
214+
let output = command
215+
.output()
216+
.with_context(|| "could not run `git rev-parse`")?;
217+
ensure!(output.status.success(), "`git rev-parse` failed");
218+
String::from_utf8(output.stdout).map_err(Into::into)
219+
}
220+
107221
fn build_afl(args: &Args, work_dir: &Path) -> Result<()> {
108222
// if you had already installed cargo-afl previously you **must** clean AFL++
109223
let afl_dir = common::afl_dir()?;

cargo-afl/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ declare_afl_subcommand_enum! {
104104
Addseeds("Invoke afl-addseeds"),
105105
Analyze("Invoke afl-analyze"),
106106
Cmin("Invoke afl-cmin"),
107-
Config("Build or rebuild AFL++", config::Args),
107+
Config("Build, rebuild, or update AFL++", config::Args),
108108
Fuzz("Invoke afl-fuzz"),
109109
Gotcpu("Invoke afl-gotcpu"),
110110
Plot("Invoke afl-plot"),

0 commit comments

Comments
 (0)