Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions crates/libafl/src/schedulers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use alloc::{borrow::ToOwned, string::ToString};
use core::{hash::Hash, marker::PhantomData};

pub mod testcase_score;
pub use testcase_score::{LenTimeMulTestcasePenalty, TestcasePenalty, TestcaseScore};
pub use testcase_score::{
GitRecencyConfigMetadata, GitRecencyMapMetadata, GitRecencyTestcaseMetadata,
GitRecencyTestcaseScore, LenTimeMulTestcasePenalty, TestcasePenalty, TestcaseScore,
};

pub mod queue;
pub use queue::QueueScheduler;
Expand All @@ -24,7 +27,7 @@ pub mod accounting;
pub use accounting::CoverageAccountingScheduler;

pub mod weighted;
pub use weighted::{StdWeightedScheduler, WeightedScheduler};
pub use weighted::{GitAwareStdWeightedScheduler, StdWeightedScheduler, WeightedScheduler};

pub mod tuneable;
use libafl_bolts::{
Expand Down
248 changes: 247 additions & 1 deletion crates/libafl/src/schedulers/testcase_score.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! The `TestcaseScore` is an evaluator providing scores of corpus items.
use alloc::string::String;
use alloc::{string::String, vec::Vec};

use libafl_bolts::{HasLen, HasRefCnt};
use num_traits::Zero;
use serde::{Deserialize, Serialize};

use crate::{
Error, HasMetadata,
Expand Down Expand Up @@ -353,3 +354,248 @@ where
Ok(weight)
}
}

/// The `git blame` timestamp mapping for `SanitizerCoverage` pc-guard map indexes.
///
/// If an index has no mapping, it is considered "old" (timestamp `0`).
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(
any(not(feature = "serdeany_autoreg"), miri),
expect(clippy::unsafe_derive_deserialize)
)] // for SerdeAny
pub struct GitRecencyMapMetadata {
/// The reference time used to compute decay (epoch seconds), taken from the `HEAD` commit time.
pub head_time: u64,
/// `entries[index] = epoch_seconds` for that `pcguard_index`.
pub entries: Vec<u64>,
}

libafl_bolts::impl_serdeany!(GitRecencyMapMetadata);

impl GitRecencyMapMetadata {
/// A fixed 14-day half-life.
pub const HALF_LIFE_SECS: u64 = 14 * 24 * 60 * 60;

/// Parse a mapping file generated by `libafl_cc`.
///
/// Format (little-endian):
/// - `u64 head_time`
/// - `u64 len`
/// - `len * u64` entries
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
const HEADER_LEN: usize = 16;
if bytes.len() < HEADER_LEN {
return Err(Error::illegal_argument(
"GitRecencyMapMetadata: mapping file too small",
));
}

let head_time = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
let len = u64::from_le_bytes(bytes[8..16].try_into().unwrap());

let Ok(len_usize) = usize::try_from(len) else {
return Err(Error::illegal_argument(
"GitRecencyMapMetadata: mapping length does not fit usize",
));
};

let expected_len = HEADER_LEN
.checked_add(len_usize.checked_mul(8).ok_or_else(|| {
Error::illegal_argument("GitRecencyMapMetadata: mapping length overflow")
})?)
.ok_or_else(|| {
Error::illegal_argument("GitRecencyMapMetadata: mapping length overflow")
})?;

if bytes.len() != expected_len {
return Err(Error::illegal_argument(format!(
"GitRecencyMapMetadata: mapping file has unexpected size (got {}, expected {})",
bytes.len(),
expected_len
)));
}

let mut entries = Vec::with_capacity(len_usize);
for i in 0..len_usize {
let start = HEADER_LEN + i * 8;
let end = start + 8;
entries.push(u64::from_le_bytes(bytes[start..end].try_into().unwrap()));
}

Ok(Self { head_time, entries })
}

/// Load a mapping file generated by `libafl_cc` from disk.
#[cfg(feature = "std")]
pub fn load_from_file(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
let bytes = std::fs::read(path).map_err(Error::from)?;
Self::from_bytes(&bytes)
}

/// Returns the `git blame` timestamp for this `pcguard_index`, or `0` if missing/out of range.
#[must_use]
pub fn timestamp_for_index(&self, idx: usize) -> u64 {
self.entries.get(idx).copied().unwrap_or(0)
}
}

/// Optional configuration for git-aware scheduling.
///
/// If not present in the state, `GitRecencyTestcaseScore` uses a default `alpha`.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(
any(not(feature = "serdeany_autoreg"), miri),
expect(clippy::unsafe_derive_deserialize)
)] // for SerdeAny
pub struct GitRecencyConfigMetadata {
/// Bias strength. Larger means "recently changed code" is picked more often.
pub alpha: f64,
}

libafl_bolts::impl_serdeany!(GitRecencyConfigMetadata);

impl GitRecencyConfigMetadata {
/// Default `alpha` used if no config metadata is present.
pub const DEFAULT_ALPHA: f64 = 2.0;

/// Create a new config with the given `alpha`.
#[must_use]
pub fn new(alpha: f64) -> Self {
Self { alpha }
}
}

impl Default for GitRecencyConfigMetadata {
fn default() -> Self {
Self {
alpha: Self::DEFAULT_ALPHA,
}
}
}

/// Cached per-testcase maximum `git blame` timestamp over its covered indices.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(
any(not(feature = "serdeany_autoreg"), miri),
expect(clippy::unsafe_derive_deserialize)
)] // for SerdeAny
pub struct GitRecencyTestcaseMetadata {
/// The max timestamp among all indices this testcase covers.
pub tc_time: u64,
}

libafl_bolts::impl_serdeany!(GitRecencyTestcaseMetadata);

/// A `TestcaseScore` that boosts corpus weights for testcases covering recently changed code.
#[derive(Debug, Clone)]
pub struct GitRecencyTestcaseScore {}

impl GitRecencyTestcaseScore {
const HALF_LIFE_SECS_F64: f64 = 14.0 * 24.0 * 60.0 * 60.0;

fn compute_decay(head_time: u64, tc_time: u64) -> f64 {
if head_time == 0 || tc_time == 0 {
return 0.0;
}

#[expect(clippy::cast_precision_loss)]
let age = head_time.saturating_sub(tc_time) as f64;
libm::exp2(-(age / Self::HALF_LIFE_SECS_F64))
}
}

impl<I, S> TestcaseScore<I, S> for GitRecencyTestcaseScore
where
S: HasCorpus<I> + HasMetadata,
{
fn compute(state: &S, entry: &mut Testcase<I>) -> Result<f64, Error> {
let base = CorpusWeightTestcaseScore::compute(state, entry)?;

let Ok(map_meta) = state.metadata::<GitRecencyMapMetadata>() else {
// No mapping loaded -> no boost.
return Ok(base);
};

let alpha = state
.metadata::<GitRecencyConfigMetadata>()
.map(|m| m.alpha)
.unwrap_or(GitRecencyConfigMetadata::DEFAULT_ALPHA);

let tc_time = if let Some(meta) = entry.metadata_map().get::<GitRecencyTestcaseMetadata>() {
meta.tc_time
} else {
let mut tc_time = 0u64;
if let Some(indexes) = entry.metadata_map().get::<MapIndexesMetadata>() {
for idx in &indexes.list {
tc_time = tc_time.max(map_meta.timestamp_for_index(*idx));
}
}
entry.add_metadata(GitRecencyTestcaseMetadata { tc_time });
tc_time
};

let decay = Self::compute_decay(map_meta.head_time, tc_time);
let boost = 1.0 + alpha * decay;
Ok(base * boost)
}
}

#[cfg(test)]
mod git_recency_tests {
use crate::{
HasMetadata,
corpus::{Corpus, InMemoryCorpus, SchedulerTestcaseMetadata, Testcase},
feedbacks::MapIndexesMetadata,
inputs::NopInput,
schedulers::{GitRecencyMapMetadata, GitRecencyTestcaseScore, TestcaseScore},
state::{HasCorpus, StdState},
};

#[test]
fn test_git_recency_score_boosts_recent() {
#[cfg(not(feature = "serdeany_autoreg"))]
unsafe {
libafl_bolts::serdeany::RegistryBuilder::register::<
crate::schedulers::powersched::SchedulerMetadata,
>();
libafl_bolts::serdeany::RegistryBuilder::register::<GitRecencyMapMetadata>();
libafl_bolts::serdeany::RegistryBuilder::register::<super::GitRecencyConfigMetadata>();
libafl_bolts::serdeany::RegistryBuilder::register::<super::GitRecencyTestcaseMetadata>(
);
libafl_bolts::serdeany::RegistryBuilder::register::<SchedulerTestcaseMetadata>();
libafl_bolts::serdeany::RegistryBuilder::register::<MapIndexesMetadata>();
}

let mut corpus = InMemoryCorpus::new();
let mut testcase = Testcase::new(NopInput {});
testcase.add_metadata(SchedulerTestcaseMetadata::new(0));
testcase.add_metadata(MapIndexesMetadata::new(vec![0, 2]));
let id = corpus.add(testcase).unwrap();

let mut state = StdState::new(
libafl_bolts::rands::StdRand::with_seed(0),
corpus,
InMemoryCorpus::new(),
&mut (),
&mut (),
)
.unwrap();

// Required by CorpusWeightTestcaseScore, even though it will early-return.
let _ = state.metadata_or_insert_with(|| {
crate::schedulers::powersched::SchedulerMetadata::new(None)
});

// Mapping: index 2 is "recent", index 0 is "old".
state.add_metadata(GitRecencyMapMetadata {
head_time: 1000,
entries: vec![1, 0, 990],
});

let mut testcase_ref = state.corpus().get(id).unwrap().borrow_mut();
let score = GitRecencyTestcaseScore::compute(&state, &mut testcase_ref).unwrap();

// Base weight is 1.0 (scheduled_count==0 or cycles==0).
assert!(score > 1.0);
}
}
5 changes: 4 additions & 1 deletion crates/libafl/src/schedulers/weighted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
AflScheduler, HasQueueCycles, RemovableScheduler, Scheduler, on_add_metadata_default,
on_evaluation_metadata_default, on_next_metadata_default,
powersched::{BaseSchedule, PowerSchedule, SchedulerMetadata},
testcase_score::{CorpusWeightTestcaseScore, TestcaseScore},
testcase_score::{CorpusWeightTestcaseScore, GitRecencyTestcaseScore, TestcaseScore},
},
state::{HasCorpus, HasRand},
};
Expand Down Expand Up @@ -390,6 +390,9 @@ where
/// The standard corpus weight, same as in `AFL++`
pub type StdWeightedScheduler<C, O> = WeightedScheduler<C, CorpusWeightTestcaseScore, O>;

/// A git-aware corpus weight scheduler that biases towards testcases covering recently changed code.
pub type GitAwareStdWeightedScheduler<C, O> = WeightedScheduler<C, GitRecencyTestcaseScore, O>;

#[cfg(test)]
mod tests {
use core::time::Duration;
Expand Down
7 changes: 7 additions & 0 deletions crates/libafl_cc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ coverage-accounting = []
cmplog-instructions = []
ctx = []
dump-cfg = []
git-recency = ["dep:object"]

[[bin]]
name = "libafl_git_recency_mapgen"
path = "src/bin/libafl_git_recency_mapgen.rs"
required-features = ["git-recency"]

[build-dependencies]
cc = { workspace = true, features = ["parallel"] }
Expand All @@ -52,6 +58,7 @@ serde = { workspace = true, default-features = false, features = [
"alloc",
"derive",
] } # serialization lib
object = { version = "0.38.1", optional = true }

[lints]
workspace = true
20 changes: 20 additions & 0 deletions crates/libafl_cc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use core::str;
feature = "cmplog-instructions",
feature = "ctx",
feature = "dump-cfg",
feature = "git-recency",
))]
use std::path::PathBuf;
use std::{env, fs::File, io::Write, path::Path, process::Command};
Expand All @@ -33,6 +34,7 @@ const LLVM_VERSION_MIN: u32 = 15;
feature = "cmplog-instructions",
feature = "ctx",
feature = "dump-cfg",
feature = "git-recency",
))]
fn dll_extension<'a>() -> &'a str {
if let Ok(vendor) = env::var("CARGO_CFG_TARGET_VENDOR")
Expand Down Expand Up @@ -187,6 +189,7 @@ fn find_llvm_version() -> Option<i32> {
feature = "cmplog-instructions",
feature = "ctx",
feature = "dump-cfg",
feature = "git-recency",
))]
#[expect(clippy::too_many_arguments)]
fn build_pass(
Expand Down Expand Up @@ -388,6 +391,11 @@ pub const LIBAFL_CC_LLVM_VERSION: Option<usize> = None;
exec_llvm_config(&["--cxxflags"])
};
let mut cxxflags: Vec<String> = cxxflags.split_whitespace().map(String::from).collect();
// Rust's LLVM is commonly built without RTTI. We also build our pass plugins without RTTI so
// they can be loaded via `rustc -Z llvm-plugins=...` for Rust targets.
if !cxxflags.iter().any(|f| f == "-fno-rtti") {
cxxflags.push("-fno-rtti".to_string());
}

let edge_map_default_size: usize = option_env!("LIBAFL_EDGES_MAP_DEFAULT_SIZE")
.map_or(Ok(65_536), str::parse)
Expand Down Expand Up @@ -570,6 +578,18 @@ pub const LIBAFL_CC_LLVM_VERSION: Option<usize> = None;
false,
);

#[cfg(feature = "git-recency")]
build_pass(
bindir_path,
out_dir,
&cxxflags,
&ldflags,
src_dir,
"git-recency-pass.cc",
None,
true,
);

cc::Build::new()
.file(src_dir.join("no-link-rt.c"))
.compile("no-link-rt");
Expand Down
Loading
Loading