Skip to content

Commit

Permalink
stream|tokio: Port proptest fuzz harnesses to use cargo-fuzz
Browse files Browse the repository at this point in the history
This change ports fuzz tests from the black-box fuzzing framework,
proptest-rs over to use the grey-box fuzzing framework cargo-fuzz.

Refs: #5391
  • Loading branch information
nathaniel-brough committed Jan 22, 2023
1 parent c90757f commit 0327538
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 82 deletions.
27 changes: 26 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ LOOM_MAX_PREEMPTIONS=1 RUSTFLAGS="--cfg loom" \

You can run miri tests with
```
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-tag-raw-pointers" PROPTEST_CASES=10 \
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-tag-raw-pointers" \
cargo +nightly miri test --features full --lib
```

Expand All @@ -209,6 +209,31 @@ utilities available to use in tests, no matter the crate being tested.
The best strategy for writing a new integration test is to look at existing
integration tests in the crate and follow the style.

#### Fuzz tests

Some of our crates include a set of fuzz tests, this will be marked by a
directory `fuzz`. It is a good idea to run fuzz tests after each change.
To get started with fuzz testing you'll need to install
[cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz).

`cargo install cargo-fuzz`

To list the available fuzzing harnesses you can run;

```bash
$ cd tokio
$ cargo fuzz list
fuzz_linked_list
````

Running a fuzz test is as simple as;

`cargo fuzz run fuzz_linked_list`

**NOTE**: Keep in mind that by default when running a fuzz test the fuzz
harness will run forever and will only exit if you `ctrl-c` or it finds
a bug.

#### Documentation tests

Ideally, every API has at least one [documentation test] that demonstrates how to
Expand Down
3 changes: 0 additions & 3 deletions tokio-stream/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ parking_lot = "0.12.0"
tokio-test = { path = "../tokio-test" }
futures = { version = "0.3", default-features = false }

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
proptest = "1"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
Expand Down
4 changes: 4 additions & 0 deletions tokio-stream/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
29 changes: 29 additions & 0 deletions tokio-stream/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "tokio-stream-fuzz"
version = "0.0.0"
publish = false
edition = "2018"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
tokio-test = { path = "../../tokio-test" }

[dependencies.tokio-stream]
path = ".."


# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[profile.release]
debug = 1

[[bin]]
name = "fuzz_stream_map"
path = "fuzz_targets/fuzz_stream_map.rs"
test = false
doc = false
80 changes: 80 additions & 0 deletions tokio-stream/fuzz/fuzz_targets/fuzz_stream_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use std::pin::Pin;

use tokio_stream::{self as stream, pending, Stream, StreamExt, StreamMap};
use tokio_test::{assert_ok, assert_pending, assert_ready, task};

macro_rules! assert_ready_some {
($($t:tt)*) => {
match assert_ready!($($t)*) {
Some(v) => v,
None => panic!("expected `Some`, got `None`"),
}
};
}

macro_rules! assert_ready_none {
($($t:tt)*) => {
match assert_ready!($($t)*) {
None => {}
Some(v) => panic!("expected `None`, got `Some({:?})`", v),
}
};
}

fn pin_box<T: Stream<Item = U> + 'static, U>(s: T) -> Pin<Box<dyn Stream<Item = U>>> {
Box::pin(s)
}

fuzz_target!(|data: &[u8]| {
use std::task::{Context, Poll};

struct DidPoll<T> {
did_poll: bool,
inner: T,
}

impl<T: Stream + Unpin> Stream for DidPoll<T> {
type Item = T::Item;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T::Item>> {
self.did_poll = true;
Pin::new(&mut self.inner).poll_next(cx)
}
}

for _ in 0..10 {
let mut map = task::spawn(StreamMap::new());
let mut expect = 0;

for (i, is_empty) in data.iter().map(|x| *x != 0).enumerate() {
let inner = if is_empty {
pin_box(stream::empty::<()>())
} else {
expect += 1;
pin_box(stream::pending::<()>())
};

let stream = DidPoll {
did_poll: false,
inner,
};

map.insert(i, stream);
}

if expect == 0 {
assert_ready_none!(map.poll_next());
} else {
assert_pending!(map.poll_next());

assert_eq!(expect, map.values().count());

for stream in map.values() {
assert!(stream.did_poll);
}
}
}
});
57 changes: 0 additions & 57 deletions tokio-stream/tests/stream_stream_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,63 +325,6 @@ fn one_ready_many_none() {
}
}

#[cfg(not(target_os = "wasi"))]
proptest::proptest! {
#[test]
fn fuzz_pending_complete_mix(kinds: Vec<bool>) {
use std::task::{Context, Poll};

struct DidPoll<T> {
did_poll: bool,
inner: T,
}

impl<T: Stream + Unpin> Stream for DidPoll<T> {
type Item = T::Item;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>)
-> Poll<Option<T::Item>>
{
self.did_poll = true;
Pin::new(&mut self.inner).poll_next(cx)
}
}

for _ in 0..10 {
let mut map = task::spawn(StreamMap::new());
let mut expect = 0;

for (i, &is_empty) in kinds.iter().enumerate() {
let inner = if is_empty {
pin_box(stream::empty::<()>())
} else {
expect += 1;
pin_box(stream::pending::<()>())
};

let stream = DidPoll {
did_poll: false,
inner,
};

map.insert(i, stream);
}

if expect == 0 {
assert_ready_none!(map.poll_next());
} else {
assert_pending!(map.poll_next());

assert_eq!(expect, map.values().count());

for stream in map.values() {
assert!(stream.did_poll);
}
}
}
}
}

fn pin_box<T: Stream<Item = U> + 'static, U>(s: T) -> Pin<Box<dyn Stream<Item = U>>> {
Box::pin(s)
}
2 changes: 0 additions & 2 deletions tokio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ mockall = "0.11.1"
tempfile = "3.1.0"
async-stream = "0.3"

[target.'cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))'.dev-dependencies]
proptest = "1"
socket2 = "0.4"

[target.'cfg(not(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown")))'.dev-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions tokio/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
29 changes: 29 additions & 0 deletions tokio/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "tokio-fuzz"
version = "0.0.0"
publish = false
edition = "2018"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.tokio]
path = ".."
features = ["fs","net","process","rt","sync","signal","time"]


# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[profile.release]
debug = 1

[[bin]]
name = "fuzz_linked_list"
path = "fuzz_targets/fuzz_linked_list.rs"
test = false
doc = false
7 changes: 7 additions & 0 deletions tokio/fuzz/fuzz_targets/fuzz_linked_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
tokio::fuzz::fuzz_linked_list(data);
});
1 change: 1 addition & 0 deletions tokio/src/fuzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use crate::util::linked_list::tests::fuzz_linked_list;
3 changes: 3 additions & 0 deletions tokio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,6 @@ cfg_macros! {
#[cfg(feature = "io-util")]
#[cfg(test)]
fn is_unpin<T: Unpin>() {}

#[cfg(fuzzing)]
pub mod fuzz;
29 changes: 10 additions & 19 deletions tokio/src/util/linked_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,9 @@ impl<T> fmt::Debug for Pointers<T> {
}
}

#[cfg(test)]
#[cfg(any(test, fuzzing))]
#[cfg(not(loom))]
mod tests {
pub(crate) mod tests {
use super::*;

use std::pin::Pin;
Expand Down Expand Up @@ -623,31 +623,22 @@ mod tests {
}
}

#[cfg(not(tokio_wasm))]
proptest::proptest! {
#[test]
fn fuzz_linked_list(ops: Vec<usize>) {
run_fuzz(ops);
}
#[derive(Debug)]
enum Op {
Push,
Pop,
Remove(usize),
}

#[cfg(not(tokio_wasm))]
fn run_fuzz(ops: Vec<usize>) {
pub fn fuzz_linked_list(ops: &[u8]) {
use std::collections::VecDeque;

#[derive(Debug)]
enum Op {
Push,
Pop,
Remove(usize),
}

let ops = ops
.iter()
.map(|i| match i % 3 {
.map(|i| match i % 3u8 {
0 => Op::Push,
1 => Op::Pop,
2 => Op::Remove(i / 3),
2 => Op::Remove((i / 3u8) as usize),
_ => unreachable!(),
})
.collect::<Vec<_>>();
Expand Down

0 comments on commit 0327538

Please sign in to comment.