Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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