8000 Add bindings for custom crossover with example · rust-fuzz/libfuzzer@c773bcf · GitHub 8000
[go: up one dir, main page]

Skip to content

Commit c773bcf

Browse files
author
R. Elliott Childre
committed
Add bindings for custom crossover with example
* Expose `LLVMFuzzerCustomCrossOver` through `fuzz_crossover` macro. * `example_crossover` uses `fuzz_mutator` & `fuzz_crossover`
1 parent 910a31a commit c773bcf

File tree

10 files changed

+347
-1
lines changed

10 files changed

+347
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Released YYYY-MM-DD.
44

55
### Added
66

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

910
### Changed
1011

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ arbitrary-derive = ["arbitrary/derive"]
2424
members = [
2525
"./example/fuzz",
2626
"./example_arbitrary/fuzz",
27+
"./example_crossover/fuzz",
2728
"./example_mutator/fuzz",
2829
]
2930

ci/script.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@ cargo fuzz build --dev
3030
(! cargo fuzz run boom -- -runs=10000000)
3131
popd
3232

33+
pushd ./example_crossover
34+
cargo fuzz build
35+
cargo fuzz build --dev
36+
(! cargo fuzz run --release boom -- -runs=10000000)
37+
popd
38+
3339
echo "All good!"

example_crossover/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
crash-*

example_crossover/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "example_crossover"
3+
version = "0.1.0"
4+
authors = ["R. Elliott Childre"]
5+
edition = "2021"
6+
7+
[target.'cfg(fuzzing)'.dependencies]
8+
rand = "0.8"

example_crossover/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# A Custom Crossover Example
2+
3+
## Overview
4+
5+
This example is a reimplementation of [Erik Rigtorp's floating point summation fuzzing example][1]
6+
in the Rust bindings for LibFuzzer, provided by this crate. In this particular example, Erik uses
7+
both a custom mutator, and a custom crossover function, which provides a well-documented, complex
8+
code example.
9+
10+
## Implementation
11+
12+
This is mostly a one-to-one rewrite of the C++ code in the blog post, with the big difference
13+
being the method of converting the raw bytes that is exposed to the custom functions, into the
14+
decoded double-precision floating-point values. Where in C++ we can simply do:
15+
16+
```c++
17+
uint8_t *Data = ...;
18+
size_t Size = ...;
19+
double *begin = (double *)Data;
20+
double *end = (double *)Data + Size / sizeof(double);
21+
```
22+
23+
In Rust, however, the task seems a bit more complex due to strictness on alignment:
24+
25+
* [Rust, how to slice into a byte array as if it were a float array? - Stack Overflow][2]
26+
* [Re-interpret slice of bytes (e.g. [u8]) as slice of [f32] - help - The Rust Programming Language Forum][3]
27+
* [How to transmute a u8 buffer to struct in Rust? - Stack Overflow][4]
28+
29+
So the casting of `Data` in the blog post's C++ are now `slice::align_to{_mut}` calls
30+
31+
[1]: https://rigtorp.se/fuzzing-floating-point-code/
32+
[2]: https://stackoverflow.com/a/73174764
33+
[3]: https://users.rust-lang.org/t/re-interpret-slice-of-bytes-e-g-u8-as-slice-of-f32/34551
34+
[4]: https://stackoverflow.com/a/59292352

example_crossover/fuzz/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "example_crossover_fuzz"
3+
version = "0.1.0"
4+
authors = ["R. Elliott Childre"]
5+
edition = "2021"
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
[dependencies]
11+
rand = "0.8"
12+
libfuzzer-sys = { path = "../.." }
13+
example_crossover = { path = ".." }
14+
15+
[[bin]]
16+
name = "boom"
17+
path = "fuzz_targets/boom.rs"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#![no_main]
2+
3+
use example_crossover::sum;
4+
use libfuzzer_sys::{fuzz_crossover, fuzz_mutator, fuzz_target};
5+
use rand::distributions::{Bernoulli, Distribution, Uniform};
6+
use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng};
7+
use std::mem::size_of;
8+
9+
fuzz_target!(|data: &[u8]| {
10+
let (_, floats, _) = unsafe { data.align_to::<f64>() };
11+
12+
let res = sum(floats);
13+
14+
assert!(
15+
!res.is_nan(),
16+
"The sum of the following f64's resulted in a NaN: {floats:?}"
17+
);
18+
});
19+
20+
fn rfp(rng: &mut StdRng) -> f64 {
21+
match Uniform::new_inclusive(0, 10).sample(rng) {
22+
0 => f64::NAN,
23+
1 => f64::MIN,
24+
2 => f64::MAX,
25+
3 => -f64::MIN,
26+
4 => -f64::MAX,
27+
5 => f64::EPSILON,
28+
6 => -f64::EPSILON,
29+
7 => f64::INFINITY,
30+
8 => f64::NEG_INFINITY,
31+
9 => 0.0,
32+
10 => Uniform::new_inclusive(-1.0, 1.0).sample(rng),
33+
_ => 0.0,
34+
}
35+
}
36+
37+
fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
38+
let mut gen = StdRng::seed_from_u64(seed.into());
39+
40+
match Uniform::new_inclusive(0, 3).sample(&mut gen) {
41+
0 => {
42+
// "Change [an] element"
43+
44+
// Not altering the size, so decode the intended space (i.e. `size`) as floats
45+
let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };
46+
47+
if !floats.is_empty() {
48+
let d = Uniform::new(0, floats.len());
49+
floats[d.sample(&mut gen)] = rfp(&mut gen);
50+
}
51+
}
52+
1 => {
53+
// "Add [an] element [to the end]"
54+
let plus_one = size + size_of::<f64>();
55+
if plus_one <= max_size {
56+
// Adding 1, f64 to the size, so decode the intended space (i.e.
57+
// `size`) plus one more (since we just checked it will fit) as floats
58+
let (_, floats, _) = unsafe { data[..plus_one].align_to_mut::<f64>() };
59+
60+
let last = floats.last_mut().unwrap();
61+
*last = rfp(&mut gen);
62+
63+
return plus_one;
64+
}
65+
}
66+
2 => {
67+
// "Delete [the end] element"
68+
69+
// Attempting to shrink the size by 1, f64, so decode the intended
70+
// space (i.e. `size`) as floats and see if we have any
71+
let (_, floats, _) = unsafe { data[..size].align_to::<f64>() };
72+
73+
if !floats.is_empty() {
74+
return size - size_of::<f64>();
75+
}
76+
}
77+
3 => {
78+
// "Shuffle [the] elements"
79+
80+
// Not altering the size, so decode the intended space (i.e. `size`) as floats
81+
let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };
82+
floats.shuffle(&mut gen);
83+
}
84+
_ => unreachable!(),
85+
};
86+
87+
size
88+
});
89+
90+
fuzz_crossover!(|data1: &[u8], data2: &[u8], out: &mut [u8], seed: u32| {
91+
let mut gen = StdRng::seed_from_u64(seed.into());
92+
93+
let bd = Bernoulli::new(0.5).unwrap();
94+
95+
// Decode each source to see how many floats we can pull with proper
96+
// alignment, and destination as to how many will fit with proper alignment
97+
//
98+
// Keep track of the unaligned prefix to `out`, as we will need to remember
99+
// that those bytes will remain prepended to the actual floats that we
100+
// write into the out buffer.
101+
let (out_pref, out_floats, _) = unsafe { out.align_to_mut::<f64>() };
102+
let (_, d1_floats, _) = unsafe { data1.align_to::<f64>() };
103+
let (_, d2_floats, _) = unsafe { data2.align_to::<f64>() };
104+
105+
// Given that the sources and destinations may have drastically fewer
106+
// available aligned floats than decoding allows for; see which has the
107+
// smallest number.
108+
let n = *[out_floats.len(), d1_floats.len(), d2_floats.len()]
109+
.iter()
110+
.min()
111+
.unwrap();
112+
113+
// Put into the destination, floats from either data1 or data2 if the
114+
// Bernoulli distribution succeeds or fails
115+
for i in 0..n {
116+
out_floats[i] = if bd.sample(&mut gen) {
117+
d1_floats[i]
118+
} else {
119+
d2_floats[i]
120+
};
121+
}
122+
123+
// Now that we have written the true floats, report back to the fuzzing
124+
// engine that we left the unaligned `out` prefix bytes at the beginning of
125+
// `out` and also then the floats that we wrote into the aligned float
126+
// section.
127+
out_pref.len() * size_of::<u8>() + n * size_of::<f64>()
128+
});

example_crossover/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub fn sum(floats: &[f64]) -> f64 {
2+
floats
3+
.iter()
4+
.fold(0.0, |a, b| if b.is_nan() { a } else { a + b })
5+
}

src/lib.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,148 @@ pub fn fuzzer_mutate(data: &mut [u8], size: usize, max_size: usize) -> usize {
538538
assert!(new_size <= data.len());
539539
new_size
540540
}
541+
542+
/// Define a custom cross-over function to combine test cases.
543+
///
544+
/// This is optional, and libFuzzer will use its own, default cross-over strategy
545+
/// if this is not provided. (As of the time of writing, this default strategy
546+
/// takes alternating byte sequences from the two test cases, to construct the
547+
/// new one) (see `FuzzerCrossOver.cpp`)
548+
///
549+
/// This could potentially be useful if your input is, for instance, a
550+
/// sequence of fixed sized, multi-byte values and the crossover could then
551+
/// merge discrete values rather than joining parts of a value.
552+
///
553+
/// ## Implementation Contract
554+
///
555+
/// The original, read-only inputs are given in the full slices of `data1`, and
556+
/// `data2` (as opposed to the, potentially, partial slice of `data` in
557+
/// [the `fuzz_mutator!` macro][crate::fuzz_mutator]).
558+
///
559+
/// You must place the new input merged from the two existing inputs' data
560+
/// into `out` and return the size of the relevant data written to that slice.
561+
///
562+
/// The deterministic requirements from [the `fuzz_mutator!` macro][crate::fuzz_mutator]
563+
/// apply as well to the `seed` parameter
564+
///
565+
/// ## Example: Floating-Point Sum NaN
566+
///
567+
/// ```no_run
568+
/// #![no_main]
569+
///
570+
/// use libfuzzer_sys::{fuzz_crossover, fuzz_mutator, fuzz_target, fuzzer_mutate};
571+
/// use rand::{rngs::StdRng, Rng, SeedableRng};
572+
/// use std::mem::size_of;
573+
///
574+
/// fuzz_target!(|data: &[u8]| {
575+
/// let (_, floats, _) = unsafe { data.align_to::<f64>() };
576+
///
577+
/// let res = floats
578+
/// .iter()
579+
/// .fold(0.0, |a, b| if b.is_nan() { a } else { a + b });
580+
///
581+
/// assert!(
582+
/// !res.is_nan(),
583+
/// "The sum of the following floats resulted in a NaN: {floats:?}"
584+
/// );
585+
/// });
586+
///
587+
/// // Inject some ...potentially problematic values to make the example close
588+
/// // more quickly.
589+
/// fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
590+
/// let mut gen = StdRng::seed_from_u64(seed.into());
591+
///
592+
/// let (_, floats, _) = unsafe { data[..size].align_to_mut::<f64>() };
593+
///
594+
/// let x = gen.gen_range(0..=1000);
595+
/// if x == 0 && !floats.is_empty() {
596+
/// floats[0] = f64::INFINITY;
597+
/// } else if x == 1000 && floats.len() > 1 {
598+
/// floats[1] = f64::NEG_INFINITY;
599+
/// } else {
600+
/// return fuzzer_mutate(data, size, max_size);
601+
/// }
602+
///
603+
/// size
604+
/// });
605+
///
606+
/// fuzz_crossover!(|data1: &[u8], data2: &[u8], out: &mut [u8], _seed: u32| {
607+
/// // Decode each source to see how many floats we can pull with proper
608+
/// // alignment, and destination as to how many will fit with proper alignment
609+
/// //
610+
/// // Keep track of the unaligned prefix to `out`, as we will need to remember
611+
/// // that those bytes will remain prepended to the actual floats that we
612+
/// // write into the out buffer.
613+
/// let (out_pref, out_floats, _) = unsafe { out.align_to_mut::<f64>() };
614+
/// let (_, d1_floats, _) = unsafe { data1.align_to::<f64>() };
615+
/// let (_, d2_floats, _) = unsafe { data2.align_to::<f64>() };
616+
///
617+
/// // Put into the destination, floats first from data1 then from data2, ...if
618+
/// // possible given the size of `out`
619+
/// let mut i: usize = 0;
620+
/// for float in d1_floats.iter().chain(d2_floats).take(out_floats.len()) {
621+
/// out_floats[i] = *float;
622+
/// i += 1;
623+
/// }
624+
///
625+
/// // Now that we have written the true floats, report back to the fuzzing
626+
/// // engine that we left the unaligned `out` prefix bytes at the beginning of
627+
/// // `out` and also then the floats that we wrote into the aligned float
628+
/// // section.
629+
/// out_pref.len() * size_of::<u8>() + i * size_of::<f64>()
630+
/// });
631+
/// ```
632+
///
633+
/// This example is a minimized version of [Erik Rigtorp's floating point summation fuzzing example][1].
634+
/// A more detailed version of this experiment can be found in the
635+
/// `example_crossover` directory.
636+
///
637+
/// [1]: https://rigtorp.se/fuzzing-floating-point-code/
638+
#[macro_export]
639+
macro_rules! fuzz_crossover {
640+
(
641+
|
642+
$data1:ident : &[u8] ,
643+
$data2:ident : &[u8] ,
644+
$out:ident : &mut [u8] ,
645+
$seed:ident : u32 $(,)*
646+
|
647+
$body:block
648+
) => {
649+
/// Auto-generated function. Do not use; only for LibFuzzer's
650+
/// consumption.
651+
#[export_name = "LLVMFuzzerCustomCrossOver"]
652+
#[doc(hidden)]
653+
pub unsafe fn rust_fuzzer_custom_crossover(
654+
$data1: *const u8,
655+
size1: usize,
656+
$data2: *const u8,
657+
size2: usize,
658+
$out: *mut u8,
659+
max_out_size: usize,
660+
$seed: std::os::raw::c_uint,
661+
) -> usize {
662+
let $data1: &[u8] = std::slice::from_raw_parts($data1, size1);
663+
let $data2: &[u8] = std::slice::from_raw_parts($data2, size2);
664+
let $out: &mut [u8] = std::slice::from_raw_parts_mut($out, max_out_size);
665+
666+
// `unsigned int` is generally a `u32`, but not on all targets. Do
667+
// an infallible (and potentially lossy, but that's okay because it
668+
// preserves determinism) conversion.
669+
let $seed = $seed as u32;
670+
671+
// Define and invoke a new, safe function so that the body doesn't
672+
// inherit `unsafe`.
673+
fn custom_crossover(
674+
$data1: &[u8],
675+
$data2: &[u8],
676+
$out: &mut [u8],
677+
$seed: u32,
678+
) -> usize {
679+
$body
680+
}
681+
682+
custom_crossover($data1, $data2, $out, $seed)
683+
}
684+
};
685+
}

0 commit comments

Comments
 (0)
0