Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bindings for custom crossover with example #115

Merged
merged 1 commit into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Released YYYY-MM-DD.

### Added

* TODO (or remove section if none)
* Bindings to `LLVMFuzzerCustomCrossOver` through the `fuzz_crossover` macro.
* `example_crossover` using both `fuzz_mutator` and `fuzz_crossover` (adapted from @rigtorp)

### Changed

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ arbitrary-derive = ["arbitrary/derive"]
members = [
"./example/fuzz",
"./example_arbitrary/fuzz",
"./example_crossover/fuzz",
"./example_mutator/fuzz",
]

Expand Down
6 changes: 6 additions & 0 deletions ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ cargo fuzz build --dev
(! cargo fuzz run boom -- -runs=10000000)
popd

pushd ./example_crossover
cargo fuzz build
cargo fuzz build --dev
(! cargo fuzz run --release boom -- -runs=10000000)
popd

echo "All good!"
1 change: 1 addition & 0 deletions example_crossover/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
crash-*
8 changes: 8 additions & 0 deletions example_crossover/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "example_crossover"
version = "0.1.0"
authors = ["R. Elliott Childre"]
edition = "2021"

[target.'cfg(fuzzing)'.dependencies]
rand = "0.8"
34 changes: 34 additions & 0 deletions example_crossover/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# A Custom Crossover Example

## Overview

This example is a reimplementation of [Erik Rigtorp's floating point summation fuzzing example][1]
in the Rust bindings for LibFuzzer, provided by this crate. In this particular example, Erik uses
both a custom mutator, and a custom crossover function, which provides a well-documented, complex
code example.

## Implementation

This is mostly a one-to-one rewrite of the C++ code in the blog post, with the big difference
being the method of converting the raw bytes that is exposed to the custom functions, into the
decoded double-precision floating-point values. Where in C++ we can simply do:

```c++
uint8_t *Data = ...;
size_t Size = ...;
double *begin = (double *)Data;
double *end = (double *)Data + Size / sizeof(double);
```

In Rust, however, the task seems a bit more complex due to strictness on alignment:

* [Rust, how to slice into a byte array as if it were a float array? - Stack Overflow][2]
* [Re-interpret slice of bytes (e.g. [u8]) as slice of [f32] - help - The Rust Programming Language Forum][3]
* [How to transmute a u8 buffer to struct in Rust? - Stack Overflow][4]

So the casting of `Data` in the blog post's C++ are now `slice::align_to{_mut}` calls

[1]: https://rigtorp.se/fuzzing-floating-point-code/
[2]: https://stackoverflow.com/a/73174764
[3]: https://users.rust-lang.org/t/re-interpret-slice-of-bytes-e-g-u8-as-slice-of-f32/34551
[4]: https://stackoverflow.com/a/59292352
17 changes: 17 additions & 0 deletions example_crossover/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "example_crossover_fuzz"
version = "0.1.0"
authors = ["R. Elliott Childre"]
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
rand = "0.8"
libfuzzer-sys = { path = "../.." }
example_crossover = { path = ".." }

[[bin]]
name = "boom"
path = "fuzz_targets/boom.rs"
128 changes: 128 additions & 0 deletions example_crossover/fuzz/fuzz_targets/boom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#![no_main]

use example_crossover::sum;
use libfuzzer_sys::{fuzz_crossover, fuzz_mutator, fuzz_target};
use rand::distributions::{Bernoulli, Distribution, Uniform};
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
use std::mem::size_of;

fuzz_target!(|data: &[u8]| {
let (_, floats, _) = unsafe { data.align_to::<f64>() };

let res = sum(floats);

assert!(
!res.is_nan(),
"The sum of the following f64's resulted in a NaN: {floats:?}"
);
});

fn rfp(rng: &mut StdRng) -> f64 {
match Uniform::new_inclusive(0, 10).sample(rng) {
0 => f64::NAN,
1 => f64::MIN,
2 => f64::MAX,
3 => -f64::MIN,
4 => -f64::MAX,
5 => f64::EPSILON,
6 => -f64::EPSILON,
7 => f64::INFINITY,
8 => f64::NEG_INFINITY,
9 => 0.0,
10 => Uniform::new_inclusive(-1.0, 1.0).sample(rng),
_ => 0.0,
}
}

fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
let mut gen = StdRng::seed_from_u64(seed.into());

match Uniform::new_inclusive(0, 3).sample(&mut gen) {
0 => {
// "Change [an] element"

// Not altering the size, so decode the intended space (i.e. `size`) as floats
let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };

if !floats.is_empty() {
let d = Uniform::new(0, floats.len());
floats[d.sample(&mut gen)] = rfp(&mut gen);
}
}
1 => {
// "Add [an] element [to the end]"
let plus_one = size + size_of::<f64>();
if plus_one <= max_size {
// Adding 1, f64 to the size, so decode the intended space (i.e.
// `size`) plus one more (since we just checked it will fit) as floats
let (_, floats, _) = unsafe { data[..plus_one].align_to_mut::<f64>() };

let last = floats.last_mut().unwrap();
*last = rfp(&mut gen);

return plus_one;
}
}
2 => {
// "Delete [the end] element"

// Attempting to shrink the size by 1, f64, so decode the intended
// space (i.e. `size`) as floats and see if we have any
let (_, floats, _) = unsafe { data[..size].align_to::<f64>() };

if !floats.is_empty() {
return size - size_of::<f64>();
}
}
3 => {
// "Shuffle [the] elements"

// Not altering the size, so decode the intended space (i.e. `size`) as floats
let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };
floats.shuffle(&mut gen);
}
_ => unreachable!(),
};

size
});

fuzz_crossover!(|data1: &[u8], data2: &[u8], out: &mut [u8], seed: u32| {
let mut gen = StdRng::seed_from_u64(seed.into());

let bd = Bernoulli::new(0.5).unwrap();

// Decode each source to see how many floats we can pull with proper
// alignment, and destination as to how many will fit with proper alignment
//
// Keep track of the unaligned prefix to `out`, as we will need to remember
// that those bytes will remain prepended to the actual floats that we
// write into the out buffer.
let (out_pref, out_floats, _) = unsafe { out.align_to_mut::<f64>() };
let (_, d1_floats, _) = unsafe { data1.align_to::<f64>() };
let (_, d2_floats, _) = unsafe { data2.align_to::<f64>() };

// Given that the sources and destinations may have drastically fewer
// available aligned floats than decoding allows for; see which has the
// smallest number.
let n = *[out_floats.len(), d1_floats.len(), d2_floats.len()]
.iter()
.min()
.unwrap();

// Put into the destination, floats from either data1 or data2 if the
// Bernoulli distribution succeeds or fails
for i in 0..n {
out_floats[i] = if bd.sample(&mut gen) {
d1_floats[i]
} else {
d2_floats[i]
};
}

// Now that we have written the true floats, report back to the fuzzing
// engine that we left the unaligned `out` prefix bytes at the beginning of
// `out` and also then the floats that we wrote into the aligned float
// section.
out_pref.len() * size_of::<u8>() + n * size_of::<f64>()
});
5 changes: 5 additions & 0 deletions example_crossover/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub fn sum(floats: &[f64]) -> f64 {
floats
.iter()
.fold(0.0, |a, b| if b.is_nan() { a } else { a + b })
}
145 changes: 145 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,148 @@ pub fn fuzzer_mutate(data: &mut [u8], size: usize, max_size: usize) -> usize {
assert!(new_size <= data.len());
new_size
}

/// Define a custom cross-over function to combine test cases.
///
/// This is optional, and libFuzzer will use its own, default cross-over strategy
/// if this is not provided. (As of the time of writing, this default strategy
/// takes alternating byte sequences from the two test cases, to construct the
/// new one) (see `FuzzerCrossOver.cpp`)
///
/// This could potentially be useful if your input is, for instance, a
/// sequence of fixed sized, multi-byte values and the crossover could then
/// merge discrete values rather than joining parts of a value.
///
/// ## Implementation Contract
///
/// The original, read-only inputs are given in the full slices of `data1`, and
/// `data2` (as opposed to the, potentially, partial slice of `data` in
/// [the `fuzz_mutator!` macro][crate::fuzz_mutator]).
///
/// You must place the new input merged from the two existing inputs' data
/// into `out` and return the size of the relevant data written to that slice.
///
/// The deterministic requirements from [the `fuzz_mutator!` macro][crate::fuzz_mutator]
/// apply as well to the `seed` parameter
///
/// ## Example: Floating-Point Sum NaN
///
/// ```no_run
/// #![no_main]
///
/// use libfuzzer_sys::{fuzz_crossover, fuzz_mutator, fuzz_target, fuzzer_mutate};
/// use rand::{rngs::StdRng, Rng, SeedableRng};
/// use std::mem::size_of;
///
/// fuzz_target!(|data: &[u8]| {
/// let (_, floats, _) = unsafe { data.align_to::<f64>() };
///
/// let res = floats
/// .iter()
/// .fold(0.0, |a, b| if b.is_nan() { a } else { a + b });
///
/// assert!(
/// !res.is_nan(),
/// "The sum of the following floats resulted in a NaN: {floats:?}"
/// );
/// });
///
/// // Inject some ...potentially problematic values to make the example close
/// // more quickly.
/// fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
/// let mut gen = StdRng::seed_from_u64(seed.into());
///
/// let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };
///
/// let x = gen.gen_range(0..=1000);
/// if x == 0 && !floats.is_empty() {
/// floats[0] = f64::INFINITY;
/// } else if x == 1000 && floats.len() > 1 {
/// floats[1] = f64::NEG_INFINITY;
/// } else {
/// return fuzzer_mutate(data, size, max_size);
/// }
///
/// size
/// });
///
/// fuzz_crossover!(|data1: &[u8], data2: &[u8], out: &mut [u8], _seed: u32| {
/// // Decode each source to see how many floats we can pull with proper
/// // alignment, and destination as to how many will fit with proper alignment
/// //
/// // Keep track of the unaligned prefix to `out`, as we will need to remember
/// // that those bytes will remain prepended to the actual floats that we
/// // write into the out buffer.
/// let (out_pref, out_floats, _) = unsafe { out.align_to_mut::<f64>() };
/// let (_, d1_floats, _) = unsafe { data1.align_to::<f64>() };
/// let (_, d2_floats, _) = unsafe { data2.align_to::<f64>() };
///
/// // Put into the destination, floats first from data1 then from data2, ...if
/// // possible given the size of `out`
/// let mut i: usize = 0;
/// for float in d1_floats.iter().chain(d2_floats).take(out_floats.len()) {
/// out_floats[i] = *float;
/// i += 1;
/// }
///
/// // Now that we have written the true floats, report back to the fuzzing
/// // engine that we left the unaligned `out` prefix bytes at the beginning of
/// // `out` and also then the floats that we wrote into the aligned float
/// // section.
/// out_pref.len() * size_of::<u8>() + i * size_of::<f64>()
/// });
/// ```
///
/// This example is a minimized version of [Erik Rigtorp's floating point summation fuzzing example][1].
/// A more detailed version of this experiment can be found in the
/// `example_crossover` directory.
///
/// [1]: https://rigtorp.se/fuzzing-floating-point-code/
#[macro_export]
macro_rules! fuzz_crossover {
(
|
$data1:ident : &[u8] ,
$data2:ident : &[u8] ,
$out:ident : &mut [u8] ,
$seed:ident : u32 $(,)*
|
$body:block
) => {
/// Auto-generated function. Do not use; only for LibFuzzer's
/// consumption.
#[export_name = "LLVMFuzzerCustomCrossOver"]
#[doc(hidden)]
pub unsafe fn rust_fuzzer_custom_crossover(
$data1: *const u8,
size1: usize,
$data2: *const u8,
size2: usize,
$out: *mut u8,
max_out_size: usize,
$seed: std::os::raw::c_uint,
) -> usize {
let $data1: &[u8] = std::slice::from_raw_parts($data1, size1);
let $data2: &[u8] = std::slice::from_raw_parts($data2, size2);
let $out: &mut [u8] = std::slice::from_raw_parts_mut($out, max_out_size);

// `unsigned int` is generally a `u32`, but not on all targets. Do
// an infallible (and potentially lossy, but that's okay because it
// preserves determinism) conversion.
let $seed = $seed as u32;

// Define and invoke a new, safe function so that the body doesn't
// inherit `unsafe`.
fn custom_crossover(
$data1: &[u8],
$data2: &[u8],
$out: &mut [u8],
$seed: u32,
) -> usize {
$body
}

custom_crossover($data1, $data2, $out, $seed)
}
};
}