From 2a3f5a59425027028f4b94a2893a94f512bcfdd0 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Mon, 6 Jan 2025 02:03:18 +0100 Subject: [PATCH] Add Bloomfilter-based Feedback for Values (#2813) * Initial commit: ValueBloomFeedback * Add test, fix feedback * Remove unneeded feedback * fix * more commit --- Cargo.toml | 1 + libafl/Cargo.toml | 5 +- libafl/src/feedbacks/mod.rs | 5 + libafl/src/feedbacks/value_bloom.rs | 190 ++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 libafl/src/feedbacks/value_bloom.rs diff --git a/Cargo.toml b/Cargo.toml index 335e8eb66b..6a834b6bd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ clap = "4.5.18" cc = "1.1.21" cmake = "0.1.51" document-features = "0.2.10" +fastbloom = { version = "0.8.0", default-features = false } hashbrown = { version = "0.14.5", default-features = false } # A faster hashmap, nostd compatible libc = "0.2.159" # For (*nix) libc libipt = "0.2.0" diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index 7781fce16d..a990e9f0c7 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -64,6 +64,9 @@ std = [ ## Tracks the Feedbacks and the Objectives that were interesting for a Testcase track_hit_feedbacks = ["std"] +## `ValueBloomFeedback` checks if an observed value has already been observed, and returns `is_interesting=true` otherwise. +value_bloom_feedback = ["fastbloom", "fastbloom/serde"] + ## Collects performance statistics of the fuzzing pipeline and displays it on `Monitor` components introspection = [] @@ -292,7 +295,7 @@ document-features = { workspace = true, optional = true } clap = { workspace = true, optional = true } num_enum = { workspace = true, optional = true } libipt = { workspace = true, optional = true } -fastbloom = { version = "0.8.0", optional = true } +fastbloom = { workspace = true, optional = true } [lints] workspace = true diff --git a/libafl/src/feedbacks/mod.rs b/libafl/src/feedbacks/mod.rs index 49e9f2e2d7..c18d08ea49 100644 --- a/libafl/src/feedbacks/mod.rs +++ b/libafl/src/feedbacks/mod.rs @@ -54,6 +54,11 @@ pub use capture_feedback::CaptureTimeoutFeedback; #[cfg(feature = "introspection")] use crate::state::HasClientPerfMonitor; +#[cfg(feature = "value_bloom_feedback")] +pub mod value_bloom; +#[cfg(feature = "value_bloom_feedback")] +pub use value_bloom::ValueBloomFeedback; + /// Feedback which initializes a state. /// /// This trait is separate from the general [`Feedback`] definition as it would not be sufficiently diff --git a/libafl/src/feedbacks/value_bloom.rs b/libafl/src/feedbacks/value_bloom.rs new file mode 100644 index 0000000000..8e9bacbf07 --- /dev/null +++ b/libafl/src/feedbacks/value_bloom.rs @@ -0,0 +1,190 @@ +//! The [`ValueBloomFeedback`] checks if a value has already been observed in a [`BloomFilter`] and returns `true` if the value is new, adding it to the bloom filter. +//! + +use core::hash::Hash; +use std::borrow::Cow; + +use fastbloom::BloomFilter; +use libafl_bolts::{ + impl_serdeany, + tuples::{Handle, MatchNameRef}, + Error, Named, +}; +use serde::{Deserialize, Serialize}; + +use super::{Feedback, StateInitializer}; +use crate::{ + executors::ExitKind, + observers::{ObserversTuple, ValueObserver}, + HasNamedMetadata, +}; + +impl_serdeany!(ValueBloomFeedbackMetadata); + +#[derive(Debug, Serialize, Deserialize)] +struct ValueBloomFeedbackMetadata { + bloom: BloomFilter, +} + +/// A Feedback that returns `true` for `is_interesting` for new values it found in a [`ValueObserver`]. +/// It keeps track of the previously seen values in a [`BloomFilter`]. +#[derive(Debug)] +pub struct ValueBloomFeedback<'a, T> { + name: Cow<'static, str>, + observer_hnd: Handle>, + #[cfg(feature = "track_hit_feedbacks")] + last_result: Option, +} + +impl<'a, T> ValueBloomFeedback<'a, T> { + /// Create a new [`ValueBloomFeedback`] + #[must_use] + pub fn new(observer_hnd: &Handle>) -> Self { + Self::with_name(observer_hnd.name().clone(), observer_hnd) + } + + /// Create a new [`ValueBloomFeedback`] with a given name + #[must_use] + pub fn with_name(name: Cow<'static, str>, observer_hnd: &Handle>) -> Self { + Self { + name, + observer_hnd: observer_hnd.clone(), + #[cfg(feature = "track_hit_feedbacks")] + last_result: None, + } + } +} + +impl Named for ValueBloomFeedback<'_, T> { + fn name(&self) -> &Cow<'static, str> { + &self.name + } +} + +impl StateInitializer for ValueBloomFeedback<'_, T> { + fn init_state(&mut self, state: &mut S) -> Result<(), Error> { + let _ = + state.named_metadata_or_insert_with::(&self.name, || { + ValueBloomFeedbackMetadata { + bloom: BloomFilter::with_false_pos(0.001).expected_items(1024), + } + }); + Ok(()) + } +} + +impl, S: HasNamedMetadata, T: Hash> Feedback + for ValueBloomFeedback<'_, T> +{ + fn is_interesting( + &mut self, + state: &mut S, + _manager: &mut EM, + _input: &I, + observers: &OT, + _exit_kind: &ExitKind, + ) -> Result { + let Some(observer) = observers.get(&self.observer_hnd) else { + return Err(Error::illegal_state(format!( + "Observer {:?} not found", + self.observer_hnd + ))); + }; + let val = observer.value.as_ref(); + + let metadata = state.named_metadata_mut::(&self.name)?; + + let res = if metadata.bloom.contains(val) { + false + } else { + metadata.bloom.insert(val); + true + }; + + #[cfg(feature = "track_hit_feedbacks")] + { + self.last_result = Some(true); + } + + Ok(res) + } + + #[cfg(feature = "track_hit_feedbacks")] + fn last_result(&self) -> Result { + self.last_result.ok_or_else(|| Error::illegal_state("No last result set in `ValueBloomFeedback`. Either `is_interesting` has never been called or the fuzzer restarted in the meantime.")) + } +} + +#[cfg(test)] +mod test { + use core::ptr::write_volatile; + + use libafl_bolts::{ownedref::OwnedRef, serdeany::NamedSerdeAnyMap, tuples::Handled}; + use tuple_list::tuple_list; + + use super::ValueBloomFeedback; + use crate::{ + events::NopEventManager, + executors::ExitKind, + feedbacks::{Feedback, StateInitializer}, + inputs::NopInput, + observers::ValueObserver, + HasNamedMetadata, + }; + + static mut VALUE: u32 = 0; + + struct NamedMetadataState { + map: NamedSerdeAnyMap, + } + + impl HasNamedMetadata for NamedMetadataState { + fn named_metadata_map(&self) -> &NamedSerdeAnyMap { + &self.map + } + + fn named_metadata_map_mut(&mut self) -> &mut NamedSerdeAnyMap { + &mut self.map + } + } + + #[test] + fn test_value_bloom_feedback() { + let value_ptr = unsafe { OwnedRef::from_ptr(&raw mut VALUE) }; + + let observer = ValueObserver::new("test_value", value_ptr); + + let mut vbf = ValueBloomFeedback::new(&observer.handle()); + + let mut state = NamedMetadataState { + map: NamedSerdeAnyMap::new(), + }; + vbf.init_state(&mut state).unwrap(); + + let observers = tuple_list!(observer); + let mut mgr = NopEventManager::::new(); + let input = NopInput {}; + let exit_ok = ExitKind::Ok; + + let first_eval = vbf + .is_interesting(&mut state, &mut mgr, &input, &observers, &exit_ok) + .unwrap(); + assert_eq!(first_eval, true); + + let second_eval = vbf + .is_interesting(&mut state, &mut mgr, &input, &observers, &exit_ok) + .unwrap(); + + assert_ne!(first_eval, second_eval); + + unsafe { + write_volatile(&raw mut VALUE, 1234_u32); + } + + let next_eval = vbf + .is_interesting(&mut state, &mut mgr, &input, &observers, &exit_ok) + .unwrap(); + assert_eq!(next_eval, true); + + } +}