From 173f9a67e7d03a9ceb08e28b3a2930d8d7691640 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Thu, 10 May 2018 14:48:02 -0700 Subject: [PATCH] Include fuzz testing setup (#274) --- .gitignore | 5 ++ Cargo.toml | 1 + tests/h2-fuzz/Cargo.toml | 14 ++++ tests/h2-fuzz/README.md | 0 tests/h2-fuzz/src/main.rs | 159 ++++++++++++++++++++++++++++++++++++++ tests/h2-tests/README.md | 16 ++++ 6 files changed, 195 insertions(+) create mode 100644 tests/h2-fuzz/Cargo.toml create mode 100644 tests/h2-fuzz/README.md create mode 100644 tests/h2-fuzz/src/main.rs diff --git a/.gitignore b/.gitignore index d5ad82d..27ae85b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ target Cargo.lock h2spec + # These are backup files generated by rustfmt **/*.rs.bk + +# Files generated by honggfuzz +hfuzz_target +hfuzz_workspace diff --git a/Cargo.toml b/Cargo.toml index 4ccfd1d..4d39a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ unstable = [] [workspace] members = [ + "tests/h2-fuzz", "tests/h2-tests", "tests/h2-support", "util/genfixture", diff --git a/tests/h2-fuzz/Cargo.toml b/tests/h2-fuzz/Cargo.toml new file mode 100644 index 0000000..1abd1e1 --- /dev/null +++ b/tests/h2-fuzz/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "h2-fuzz" +version = "0.0.0" +publish = false +license = "MIT" + +[dependencies] +h2 = { path = "../.." } + +env_logger = { version = "0.5.3", default-features = false } +futures = "0.1.21" +honggfuzz = "0.5" +http = "0.1.3" +tokio-io = "0.1.4" diff --git a/tests/h2-fuzz/README.md b/tests/h2-fuzz/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/h2-fuzz/src/main.rs b/tests/h2-fuzz/src/main.rs new file mode 100644 index 0000000..10837f8 --- /dev/null +++ b/tests/h2-fuzz/src/main.rs @@ -0,0 +1,159 @@ +#[macro_use] +extern crate futures; +extern crate tokio_io; +#[macro_use] +extern crate honggfuzz; +extern crate env_logger; +extern crate h2; +extern crate http; + +use futures::prelude::*; +use futures::{executor, future, task}; +use http::{Method, Request}; +use std::cell::Cell; +use std::io::{self, Read, Write}; +use std::sync::Arc; +use tokio_io::{AsyncRead, AsyncWrite}; + +struct MockIo<'a> { + input: &'a [u8], +} + +impl<'a> MockIo<'a> { + fn next_byte(&mut self) -> Option { + if let Some(&c) = self.input.first() { + self.input = &self.input[1..]; + Some(c) + } else { + None + } + } + + fn next_u32(&mut self) -> u32 { + (self.next_byte().unwrap_or(0) as u32) << 8 | self.next_byte().unwrap_or(0) as u32 + } +} + +impl<'a> Read for MockIo<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut len = self.next_u32() as usize; + if self.input.is_empty() { + Ok(0) + } else if len == 0 { + task::current().notify(); + Err(io::ErrorKind::WouldBlock.into()) + } else { + if len > self.input.len() { + len = self.input.len(); + } + + if len > buf.len() { + len = buf.len(); + } + buf[0..len].copy_from_slice(&self.input[0..len]); + self.input = &self.input[len..]; + Ok(len) + } + } +} + +impl<'a> AsyncRead for MockIo<'a> { + unsafe fn prepare_uninitialized_buffer(&self, _buf: &mut [u8]) -> bool { + false + } +} + +impl<'a> Write for MockIo<'a> { + fn write(&mut self, buf: &[u8]) -> Result { + let len = std::cmp::min(self.next_u32() as usize, buf.len()); + if len == 0 { + if self.input.is_empty() { + Err(io::ErrorKind::BrokenPipe.into()) + } else { + task::current().notify(); + Err(io::ErrorKind::WouldBlock.into()) + } + } else { + Ok(len) + } + } + + fn flush(&mut self) -> Result<(), io::Error> { + Ok(()) + } +} + +impl<'a> AsyncWrite for MockIo<'a> { + fn shutdown(&mut self) -> Poll<(), io::Error> { + Ok(Async::Ready(())) + } +} + +struct MockNotify { + notified: Cell, +} + +unsafe impl Sync for MockNotify {} + +impl executor::Notify for MockNotify { + fn notify(&self, _id: usize) { + self.notified.set(true); + } +} + +impl MockNotify { + fn take_notify(&self) -> bool { + self.notified.replace(false) + } +} + +fn run(script: &[u8]) -> Result<(), h2::Error> { + let notify = Arc::new(MockNotify { + notified: Cell::new(false), + }); + let notify_handle: executor::NotifyHandle = notify.clone().into(); + let io = MockIo { input: script }; + let (mut h2, mut connection) = h2::client::handshake(io).wait()?; + let mut in_progress = None; + let future = future::poll_fn(|| { + if let Async::Ready(()) = connection.poll()? { + return Ok(Async::Ready(())); + } + if in_progress.is_none() { + try_ready!(h2.poll_ready()); + let request = Request::builder() + .method(Method::POST) + .uri("https://example.com/") + .body(()) + .unwrap(); + let (resp, mut send) = h2.send_request(request, false)?; + send.send_data(vec![0u8; 32769].into(), true).unwrap(); + drop(send); + in_progress = Some(resp); + } + match in_progress.as_mut().unwrap().poll() { + r @ Ok(Async::Ready(_)) | r @ Err(_) => { + eprintln!("{:?}", r); + in_progress = None; + } + Ok(Async::NotReady) => (), + } + Ok::<_, h2::Error>(Async::NotReady) + }); + let mut spawn = executor::spawn(future); + loop { + if let Async::Ready(()) = spawn.poll_future_notify(¬ify_handle, 0)? { + return Ok(()); + } + assert!(notify.take_notify()); + } +} + +fn main() { + env_logger::init(); + loop { + fuzz!(|data: &[u8]| { + eprintln!("{:?}", run(data)); + }); + } +} diff --git a/tests/h2-tests/README.md b/tests/h2-tests/README.md index d044708..9780019 100644 --- a/tests/h2-tests/README.md +++ b/tests/h2-tests/README.md @@ -5,3 +5,19 @@ crate because they transitively depend on the `unstable` feature flag via `h2-support`. Due to a cargo limitation, if these tests existed as part of the `h2` crate, it would require that `h2-support` be published to crates.io and force the `unstable` feature flag to always be on. + +## Setup + +Install honggfuzz for cargo: + +```rust +cargo install honggfuzz +``` + +## Running + +From within this directory, run the following command: + +``` +HFUZZ_RUN_ARGS="-t 1" cargo hfuzz run h2-fuzz +```