diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..572e03b --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ + +target +corpus +artifacts diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..ca32138 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,43 @@ + +[package] +name = "h2-oss-fuzz" +version = "0.0.0" +authors = [ "David Korczynski " ] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } +libfuzzer-sys = { version = "0.4.0", features = ["arbitrary-derive"] } +tokio = { version = "1", features = [ "full" ] } +bytes = "0.5.2" +h2 = { path = "../", features = [ "unstable" ] } +h2-support = { path = "../tests/h2-support" } +futures = { version = "0.3", default-features = false, features = ["std"] } +http = "0.2" +env_logger = { version = "0.5.3", default-features = false } + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "fuzz_client" +path = "fuzz_targets/fuzz_client.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_hpack" +path = "fuzz_targets/fuzz_hpack.rs" +test = false +doc = false + +[[bin]] +name = "fuzz_e2e" +path = "fuzz_targets/fuzz_e2e.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_client.rs b/fuzz/fuzz_targets/fuzz_client.rs new file mode 100644 index 0000000..8d558a9 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_client.rs @@ -0,0 +1,31 @@ +#![no_main] +use h2_support::prelude::*; +use libfuzzer_sys::{arbitrary::Arbitrary, fuzz_target}; + +#[derive(Debug, Arbitrary)] +struct HttpSpec { + uri: Vec, + header_name: Vec, + header_value: Vec, +} + +async fn fuzz_entry(inp: HttpSpec) { + if let Ok(req) = Request::builder() + .uri(&inp.uri[..]) + .header(&inp.header_name[..], &inp.header_value[..]) + .body(()) + { + let (io, mut _srv) = mock::new(); + let (mut client, _h2) = client::Builder::new() + .handshake::<_, Bytes>(io) + .await + .unwrap(); + + let (_, _) = client.send_request(req, true).unwrap(); + } +} + +fuzz_target!(|inp: HttpSpec| { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(fuzz_entry(inp)); +}); diff --git a/fuzz/fuzz_targets/fuzz_e2e.rs b/fuzz/fuzz_targets/fuzz_e2e.rs new file mode 100644 index 0000000..a8d021a --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_e2e.rs @@ -0,0 +1,129 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use futures::future; +use futures::stream::FuturesUnordered; +use futures::Stream; +use http::{Method, Request}; +use std::future::Future; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +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> AsyncRead for MockIo<'a> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf, + ) -> Poll> { + + + let mut len = self.next_u32() as usize; + if self.input.is_empty() { + Poll::Ready(Ok(())) + } else if len == 0 { + cx.waker().clone().wake(); + Poll::Pending + } else { + if len > self.input.len() { + len = self.input.len(); + } + + if len > buf.remaining() { + len = buf.remaining(); + } + buf.put_slice(&self.input[len..]); + self.input = &self.input[len..]; + Poll::Ready(Ok(())) + } + } +} + +impl<'a> AsyncWrite for MockIo<'a> { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let len = std::cmp::min(self.next_u32() as usize, buf.len()); + if len == 0 { + if self.input.is_empty() { + Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) + } else { + cx.waker().clone().wake(); + Poll::Pending + } + } else { + Poll::Ready(Ok(len)) + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +async fn run(script: &[u8]) -> Result<(), h2::Error> { + let io = MockIo { input: script }; + let (mut h2, mut connection) = h2::client::handshake(io).await?; + let mut futs = FuturesUnordered::new(); + let future = future::poll_fn(|cx| { + if let Poll::Ready(()) = Pin::new(&mut connection).poll(cx)? { + return Poll::Ready(Ok::<_, h2::Error>(())); + } + while futs.len() < 128 { + if !h2.poll_ready(cx)?.is_ready() { + break; + } + 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); + futs.push(resp); + } + loop { + match Pin::new(&mut futs).poll_next(cx) { + Poll::Pending | Poll::Ready(None) => break, + r @ Poll::Ready(Some(Ok(_))) | r @ Poll::Ready(Some(Err(_))) => { + eprintln!("{:?}", r); + } + } + } + Poll::Pending + }); + future.await?; + Ok(()) +} + +fuzz_target!(|data: &[u8]| { + let rt = tokio::runtime::Runtime::new().unwrap(); + let _res = rt.block_on(run(data)); +}); + diff --git a/fuzz/fuzz_targets/fuzz_hpack.rs b/fuzz/fuzz_targets/fuzz_hpack.rs new file mode 100644 index 0000000..c2597bb --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_hpack.rs @@ -0,0 +1,6 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data_: &[u8]| { + let _decoder_ = h2::fuzz_bridge::fuzz_logic::fuzz_hpack(data_); +}); diff --git a/src/fuzz_bridge.rs b/src/fuzz_bridge.rs new file mode 100644 index 0000000..5379a4d --- /dev/null +++ b/src/fuzz_bridge.rs @@ -0,0 +1,28 @@ +#[cfg(fuzzing)] +pub mod fuzz_logic { + use crate::hpack; + use bytes::{BufMut, BytesMut}; + use http::header::HeaderName; + use std::io::Cursor; + + pub fn fuzz_hpack(data_: &[u8]) { + let mut decoder_ = hpack::Decoder::new(0); + let mut buf = BytesMut::new(); + buf.extend(data_); + let _dec_res = decoder_.decode(&mut Cursor::new(&mut buf), |_h| {}); + + if let Ok(s) = std::str::from_utf8(data_) { + if let Ok(h) = http::Method::from_bytes(s.as_bytes()) { + let m_ = hpack::Header::Method(h); + let mut encoder = hpack::Encoder::new(0, 0); + let _res = encode(&mut encoder, vec![m_]); + } + } + } + + fn encode(e: &mut hpack::Encoder, hdrs: Vec>>) -> BytesMut { + let mut dst = BytesMut::with_capacity(1024); + e.encode(None, &mut hdrs.into_iter(), &mut (&mut dst).limit(1024)); + dst + } +} diff --git a/src/lib.rs b/src/lib.rs index 88a63de..2942ac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,10 @@ pub mod client; pub mod server; mod share; +#[cfg(fuzzing)] +#[cfg_attr(feature = "unstable", allow(missing_docs))] +pub mod fuzz_bridge; + pub use crate::error::{Error, Reason}; pub use crate::share::{FlowControl, Ping, PingPong, Pong, RecvStream, SendStream, StreamId}; diff --git a/tests/h2-fuzz/Cargo.toml b/tests/h2-fuzz/Cargo.toml index 7fbf4c3..524627f 100644 --- a/tests/h2-fuzz/Cargo.toml +++ b/tests/h2-fuzz/Cargo.toml @@ -12,4 +12,4 @@ env_logger = { version = "0.5.3", default-features = false } futures = { version = "0.3", default-features = false, features = ["std"] } honggfuzz = "0.5" http = "0.2" -tokio = "1" +tokio = { version = "1", features = [ "full" ] } diff --git a/tests/h2-fuzz/src/main.rs b/tests/h2-fuzz/src/main.rs index a57fb76..e67a540 100644 --- a/tests/h2-fuzz/src/main.rs +++ b/tests/h2-fuzz/src/main.rs @@ -1,132 +1,128 @@ -use futures::future; -use futures::stream::FuturesUnordered; -use futures::Stream; -use http::{Method, Request}; -use std::future::Future; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; -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> AsyncRead for MockIo<'a> { - unsafe fn prepare_uninitialized_buffer(&self, _buf: &mut [std::mem::MaybeUninit]) -> bool { - false - } - - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - let mut len = self.next_u32() as usize; - if self.input.is_empty() { - Poll::Ready(Ok(0)) - } else if len == 0 { - cx.waker().clone().wake(); - Poll::Pending - } 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..]; - Poll::Ready(Ok(len)) - } - } -} - -impl<'a> AsyncWrite for MockIo<'a> { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let len = std::cmp::min(self.next_u32() as usize, buf.len()); - if len == 0 { - if self.input.is_empty() { - Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) - } else { - cx.waker().clone().wake(); - Poll::Pending - } - } else { - Poll::Ready(Ok(len)) - } - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -async fn run(script: &[u8]) -> Result<(), h2::Error> { - let io = MockIo { input: script }; - let (mut h2, mut connection) = h2::client::handshake(io).await?; - let mut futs = FuturesUnordered::new(); - let future = future::poll_fn(|cx| { - if let Poll::Ready(()) = Pin::new(&mut connection).poll(cx)? { - return Poll::Ready(Ok::<_, h2::Error>(())); - } - while futs.len() < 128 { - if !h2.poll_ready(cx)?.is_ready() { - break; - } - 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); - futs.push(resp); - } - loop { - match Pin::new(&mut futs).poll_next(cx) { - Poll::Pending | Poll::Ready(None) => break, - r @ Poll::Ready(Some(Ok(_))) | r @ Poll::Ready(Some(Err(_))) => { - eprintln!("{:?}", r); - } - } - } - Poll::Pending - }); - future.await?; - Ok(()) -} - -fn main() { - env_logger::init(); - let mut rt = tokio::runtime::Runtime::new().unwrap(); - loop { - honggfuzz::fuzz!(|data: &[u8]| { - eprintln!("{:?}", rt.block_on(run(data))); - }); - } -} +use futures::future; +use futures::stream::FuturesUnordered; +use futures::Stream; +use http::{Method, Request}; +use std::future::Future; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +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> AsyncRead for MockIo<'a> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf, + ) -> Poll> { + let mut len = self.next_u32() as usize; + if self.input.is_empty() { + Poll::Ready(Ok(())) + } else if len == 0 { + cx.waker().clone().wake(); + Poll::Pending + } else { + if len > self.input.len() { + len = self.input.len(); + } + + if len > buf.remaining() { + len = buf.remaining(); + } + buf.put_slice(&self.input[len..]); + self.input = &self.input[len..]; + Poll::Ready(Ok(())) + } + } +} + +impl<'a> AsyncWrite for MockIo<'a> { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let len = std::cmp::min(self.next_u32() as usize, buf.len()); + if len == 0 { + if self.input.is_empty() { + Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())) + } else { + cx.waker().clone().wake(); + Poll::Pending + } + } else { + Poll::Ready(Ok(len)) + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +async fn run(script: &[u8]) -> Result<(), h2::Error> { + let io = MockIo { input: script }; + let (mut h2, mut connection) = h2::client::handshake(io).await?; + let mut futs = FuturesUnordered::new(); + let future = future::poll_fn(|cx| { + if let Poll::Ready(()) = Pin::new(&mut connection).poll(cx)? { + return Poll::Ready(Ok::<_, h2::Error>(())); + } + while futs.len() < 128 { + if !h2.poll_ready(cx)?.is_ready() { + break; + } + 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); + futs.push(resp); + } + loop { + match Pin::new(&mut futs).poll_next(cx) { + Poll::Pending | Poll::Ready(None) => break, + r @ Poll::Ready(Some(Ok(_))) | r @ Poll::Ready(Some(Err(_))) => { + eprintln!("{:?}", r); + } + } + } + Poll::Pending + }); + future.await?; + Ok(()) +} + +fn main() { + env_logger::init(); + let mut rt = tokio::runtime::Runtime::new().unwrap(); + loop { + honggfuzz::fuzz!(|data: &[u8]| { + eprintln!("{:?}", rt.block_on(run(data))); + }); + } +}