diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index a185a9f8..f7923916 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## [Unreleased] - xxx +### Fixed +* Potential UB in h1 decoder using uninitialized memory. [#1614] + +[#1614]: https://github.com/actix/actix-web/pull/1614 + ## [2.0.0-beta.1] - 2020-07-11 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 232b5c3f..bbb2a214 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -103,3 +103,7 @@ harness = false [[bench]] name = "status-line" harness = false + +[[bench]] +name = "uninit-headers" +harness = false diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs new file mode 100644 index 00000000..83e74171 --- /dev/null +++ b/actix-http/benches/uninit-headers.rs @@ -0,0 +1,137 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use bytes::BytesMut; + +// A Miri run detects UB, seen on this playground: +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915 + +fn bench_header_parsing(c: &mut Criterion) { + c.bench_function("Original (Unsound) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _new::parse_headers(&mut buf); + }) + }); + + c.bench_function("Original (Unsound) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _new::parse_headers(&mut buf); + }) + }); +} + +criterion_group!(benches, bench_header_parsing); +criterion_main!(benches); + +const MAX_HEADERS: usize = 96; + +const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + +#[derive(Clone, Copy)] +struct HeaderIndex { + name: (usize, usize), + value: (usize, usize), +} + +const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +impl HeaderIndex { + fn record( + bytes: &[u8], + headers: &[httparse::Header<'_>], + indices: &mut [HeaderIndex], + ) { + let bytes_ptr = bytes.as_ptr() as usize; + for (header, indices) in headers.iter().zip(indices.iter_mut()) { + let name_start = header.name.as_ptr() as usize - bytes_ptr; + let name_end = name_start + header.name.len(); + indices.name = (name_start, name_end); + let value_start = header.value.as_ptr() as usize - bytes_ptr; + let value_end = value_start + header.value.len(); + indices.value = (value_start, value_end); + } + } +} + +// test cases taken from: +// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs + +const REQ_SHORT: &'static [u8] = b"\ +GET / HTTP/1.0\r\n\ +Host: example.com\r\n\ +Cookie: session=60; user_id=1\r\n\r\n"; + +const REQ: &'static [u8] = b"\ +GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ +Host: www.kittyhell.com\r\n\ +User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ +Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\ +Accept-Encoding: gzip,deflate\r\n\ +Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\ +Keep-Alive: 115\r\n\ +Connection: keep-alive\r\n\ +Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n"; + +mod _new { + use super::*; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} + +mod _original { + use super::*; + + use std::mem::MaybeUninit; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 89a18aea..a6c52d35 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,7 +1,6 @@ use std::convert::TryFrom; use std::io; use std::marker::PhantomData; -use std::mem::MaybeUninit; use std::task::Poll; use actix_codec::Decoder; @@ -77,7 +76,7 @@ pub(crate) trait MessageType: Sized { let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); - // Unsafe: httparse check header value for valid utf-8 + // SAFETY: httparse checks header value is valid UTF-8 let value = unsafe { HeaderValue::from_maybe_shared_unchecked( slice.slice(idx.value.0..idx.value.1), @@ -184,16 +183,11 @@ impl MessageType for Request { &mut self.head_mut().headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut req = httparse::Request::new(&mut parsed); match req.parse(src)? { @@ -260,16 +254,11 @@ impl MessageType for ResponseHead { &mut self.headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, ver, status, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut res = httparse::Response::new(&mut parsed); match res.parse(src)? { @@ -324,6 +313,17 @@ pub(crate) struct HeaderIndex { pub(crate) value: (usize, usize), } +pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + impl HeaderIndex { pub(crate) fn record( bytes: &[u8], @@ -973,7 +973,7 @@ mod tests { unreachable!("Error"); } - // type in chunked + // intentional typo in "chunked" let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ transfer-encoding: chnked\r\n\r\n",